Compare commits

..

1 Commits

Author SHA1 Message Date
ahrefabhi
63b9331c78 feat: added initial session recording changes 2025-09-01 12:06:21 +05:30
43 changed files with 5041 additions and 958 deletions

View File

@@ -26,7 +26,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -48,8 +48,6 @@
"@signozhq/design-tokens": "1.1.4",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/sonner": "0.1.0",
"@signozhq/table": "0.3.4",
"@tanstack/react-table": "8.20.6",
"@tanstack/react-virtual": "3.11.2",
"@uiw/codemirror-theme-copilot": "4.23.11",
@@ -135,6 +133,7 @@
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"rehype-raw": "7.0.0",
"rrweb-player": "1.0.0-alpha.4",
"stream": "^0.0.2",
"style-loader": "1.3.0",
"styled-components": "^5.3.11",
@@ -175,6 +174,7 @@
"@commitlint/config-conventional": "^16.2.4",
"@faker-js/faker": "9.3.0",
"@jest/globals": "^27.5.1",
"@rrweb/types": "2.0.0-alpha.18",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",

View File

@@ -295,3 +295,15 @@ export const MetricsExplorer = Loadable(
export const ApiMonitoring = Loadable(
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
);
export const SessionRecordings = Loadable(
() =>
import(/* webpackChunkName: "SessionRecordings" */ 'pages/SessionRecording'),
);
export const SessionRecordingsDetail = Loadable(
() =>
import(
/* webpackChunkName: "SessionRecordingsDetail" */ 'pages/SessionRecording/SessionDetail'
),
);

View File

@@ -54,6 +54,8 @@ import {
WorkspaceAccessRestricted,
WorkspaceBlocked,
WorkspaceSuspended,
SessionRecordings,
SessionRecordingsDetail,
} from './pageComponents';
const routes: AppRoutes[] = [
@@ -450,6 +452,20 @@ const routes: AppRoutes[] = [
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.SESSION_RECORDINGS,
exact: true,
component: SessionRecordings,
key: 'SESSION_RECORDINGS',
isPrivate: true,
},
{
path: ROUTES.SESSION_RECORDINGS_DETAIL,
exact: true,
component: SessionRecordingsDetail,
key: 'SESSION_RECORDINGS_DETAIL',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,

View File

@@ -23,7 +23,6 @@ import {
} from 'container/LogDetailedView/utils';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
@@ -40,7 +39,7 @@ import {
TextSelect,
X,
} from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCopyToClipboard, useLocation } from 'react-use';
import { AppState } from 'store/reducers';
@@ -95,8 +94,6 @@ function LogDetailInner({
const { notifications } = useNotifications();
const { onLogCopy } = useCopyLogLink(log?.id);
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
const handleModeChange = (e: RadioChangeEvent): void => {
@@ -149,34 +146,6 @@ function LogDetailInner({
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
};
const handleQueryExpressionChange = useCallback(
(value: string, queryIndex: number) => {
// update the query at the given index
setContextQuery((prev) => {
if (!prev) return prev;
return {
...prev,
builder: {
...prev.builder,
queryData: prev.builder.queryData.map((query, idx) =>
idx === queryIndex
? {
...query,
filter: {
...query.filter,
expression: value,
},
}
: query,
),
},
};
});
},
[],
);
const handleRunQuery = (expression: string): void => {
let updatedContextQuery = cloneDeep(contextQuery);
@@ -336,19 +305,11 @@ function LogDetailInner({
onClick={handleFilterVisible}
/>
)}
<Tooltip title="Copy Log Link" placement="left" aria-label="Copy Log Link">
<Button
className="action-btn"
icon={<Copy size={16} />}
onClick={onLogCopy}
/>
</Tooltip>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (
<div className="log-detail-drawer-query-container">
<QuerySearch
onChange={(value): void => handleQueryExpressionChange(value, 0)}
onChange={(): void => {}}
dataSource={DataSource.LOGS}
queryData={contextQuery?.builder.queryData[0]}
onRun={handleRunQuery}

View File

@@ -56,8 +56,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
.map(({ name }) => ({
title: name,
dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replace(/\./g, '_'),
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
@@ -85,10 +83,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
// We do not need any title and data index for the log state indicator
title: '',
dataIndex: '',
// eslint-disable-next-line sonarjs/no-duplicate-string
key: 'state-indicator',
accessorKey: 'state-indicator',
id: 'state-indicator',
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<div className={cx('state-indicator', fontSize)}>
@@ -106,8 +101,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
accessorKey: 'timestamp',
id: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (
field: string | number,
@@ -142,8 +135,6 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: 'body',
dataIndex: 'body',
key: 'body',
accessorKey: 'body',
id: 'body',
render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({

View File

@@ -33,5 +33,4 @@ export enum LOCALSTORAGE {
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
FUNNEL_STEPS = 'FUNNEL_STEPS',
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
}

View File

@@ -19,6 +19,8 @@ const ROUTES = {
GET_STARTED_AZURE_MONITORING: '/get-started/azure-monitoring',
USAGE_EXPLORER: '/usage-explorer',
APPLICATION: '/services',
SESSION_RECORDINGS: '/session-recordings',
SESSION_RECORDINGS_DETAIL: '/session-recordings/:sessionId',
ALL_DASHBOARD: '/dashboard',
DASHBOARD: '/dashboard/:dashboardId',
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',

View File

@@ -4,7 +4,6 @@
import './AppLayout.styles.scss';
import * as Sentry from '@sentry/react';
import { Toaster } from '@signozhq/sonner';
import { Flex } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
@@ -853,8 +852,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
{showChangelogModal && changelog && (
<ChangelogModal changelog={changelog} onClose={toggleChangelogModal} />
)}
<Toaster />
</Layout>
);
}

View File

@@ -1,6 +1,6 @@
.explorer-options-container {
position: fixed;
bottom: 0px;
bottom: 8px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
@@ -74,7 +74,6 @@
display: flex;
gap: 16px;
z-index: 1;
.ant-select-selector {
padding: 0 !important;
}

View File

@@ -27,7 +27,7 @@
}
.explorer-show-btn {
border-radius: 6px 6px 0px 0px;
border-radius: 10px 10px 0px 0px;
border: 1px solid var(--bg-slate-400);
background: rgba(22, 24, 29, 0.4);
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.25);

View File

@@ -24,7 +24,7 @@ import { AnimatePresence } from 'motion/react';
import * as motion from 'motion/react-client';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { UserPreference } from 'types/api/preferences/preference';
import { DataSource } from 'types/common/queryBuilder';
@@ -40,6 +40,9 @@ import HomeChecklist, { ChecklistItem } from './HomeChecklist/HomeChecklist';
import SavedViews from './SavedViews/SavedViews';
import Services from './Services/Services';
import StepsProgress from './StepsProgress/StepsProgress';
import events from './rows.json';
import { eventWithTime } from '@rrweb/types';
import RRWebPlayer from 'pages/SessionRecording/RRWebPlayer';
const homeInterval = 30 * 60 * 1000;
@@ -168,6 +171,37 @@ export default function Home(): JSX.Element {
false,
);
const playerContainerRef = useRef(null);
// const [player, setPlayer] = useState<init | null>(null);
const [parsedEvents, setParsedEvents] = useState<eventWithTime[]>([]);
useEffect(() => {
if (events.rows.length > 0) {
// Initialize the rrweb player with events
const parsedEvents = events.rows.map((event) => {
return JSON.parse(event.data.body);
});
setParsedEvents(parsedEvents);
// const replayPlayer = new init({
// target: (playerContainerRef.current as unknown) as HTMLElement,
// props: {
// events: parsedEvents, // Pass the captured events to the player
// speed: 1, // Normal speed (can be adjusted)
// showDebug: false, // Optionally show debug info
// },
// });
// // Save the player instance for future use if needed
// setPlayer(replayPlayer);
// // Cleanup on unmount
// return () => {
// replayPlayer.destroy();
// };
}
}, [events]); // Re-run effect when events change
const processUserPreferences = (userPreferences: UserPreference[]): void => {
const checklistSkipped = Boolean(
userPreferences?.find(
@@ -311,6 +345,9 @@ export default function Home(): JSX.Element {
return (
<div className="home-container">
<p>hello world</p>
<RRWebPlayer events={parsedEvents} options={{ autoPlay: false }} />
{/*
<div className="sticky-header">
{showBanner && (
<div className="home-container-banner">
@@ -378,7 +415,6 @@ export default function Home(): JSX.Element {
}
/>
</div>
<div className="home-content">
<div className="home-left-content">
<DataSourceInfo
@@ -764,7 +800,7 @@ export default function Home(): JSX.Element {
</>
)}
</div>
</div>
</div> */}
</div>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -14,7 +14,4 @@ export const ContentWrapper = styled(Row)`
export const Wrapper = styled.div`
padding-bottom: 4rem;
padding-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
`;

View File

@@ -0,0 +1,14 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const CardStyled = styled(Card)`
border: none !important;
position: relative;
margin-bottom: 16px;
.ant-card-body {
height: 200px;
min-height: 200px;
padding: 0 16px 16px 16px;
font-family: 'Geist Mono';
}
`;

View File

@@ -1,25 +0,0 @@
.logs-frequency-chart-container {
height: 200px;
min-height: 200px;
border-bottom: 1px solid #262626;
.ant-card-body {
height: 200px;
min-height: 200px;
padding: 0 16px 16px 16px;
font-family: 'Geist Mono';
}
.logs-frequency-chart-loading {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
.lightMode {
.logs-frequency-chart-container {
border-bottom: 1px solid var(--bg-vanilla-300);
}
}

View File

@@ -1,5 +1,3 @@
import './LogsExplorerChart.styles.scss';
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query';
@@ -17,6 +15,7 @@ import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
import { CardStyled } from './LogsExplorerChart.styled';
import { getColorsForSeverityLabels } from './utils';
function LogsExplorerChart({
@@ -101,11 +100,9 @@ function LogsExplorerChart({
);
return (
<div className={`${className} logs-frequency-chart-container`}>
<CardStyled className={className}>
{isLoading ? (
<div className="logs-frequency-chart-loading">
<Spinner size="default" height="100%" />
</div>
<Spinner size="default" height="100%" />
) : (
<Graph
name="logsExplorerChart"
@@ -118,7 +115,7 @@ function LogsExplorerChart({
maxTime={chartMaxTime}
/>
)}
</div>
</CardStyled>
);
}

View File

@@ -1,240 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ColumnDef, DataTable, Row } from '@signozhq/table';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import LogStateIndicator from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { getLogIndicatorTypeForTable } from 'components/Logs/LogStateIndicator/utils';
import { useTableView } from 'components/Logs/TableView/useTableView';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { FontSize } from 'container/OptionsMenu/types';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import useDragColumns from 'hooks/useDragColumns';
import { getDraggedColumns } from 'hooks/useDragColumns/utils';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { isEmpty, isEqual } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { ILog } from 'types/api/logs/log';
interface ColumnViewProps {
logs: ILog[];
onLoadMore: () => void;
selectedFields: any[];
isLoading: boolean;
isFetching: boolean;
isFrequencyChartVisible: boolean;
options: {
maxLinesPerRow: number;
fontSize: FontSize;
};
}
function ColumnView({
logs,
onLoadMore,
selectedFields,
isLoading,
isFetching,
isFrequencyChartVisible,
options,
}: ColumnViewProps): JSX.Element {
const {
activeLog,
onSetActiveLog: handleSetActiveLog,
onClearActiveLog: handleClearActiveLog,
onAddToQuery: handleAddToQuery,
onGroupByAttribute: handleGroupByAttribute,
} = useActiveLog();
const { queryData: activeLogId } = useUrlQueryData<string | null>(
QueryParams.activeLogId,
null,
);
const scrollToIndexRef = useRef<
| ((
rowIndex: number,
options?: { align?: 'start' | 'center' | 'end' },
) => void)
| undefined
>();
useEffect(() => {
if (activeLogId) {
const log = logs.find(({ id }) => id === activeLogId);
if (log) {
handleSetActiveLog(log);
}
}
}, [activeLogId, logs, handleSetActiveLog]);
const tableViewProps = {
logs,
fields: selectedFields,
linesPerRow: options.maxLinesPerRow as number,
fontSize: options.fontSize as FontSize,
appendTo: 'end' as const,
activeLogIndex: 0,
};
const { dataSource, columns } = useTableView({
...tableViewProps,
onClickExpand: handleSetActiveLog,
onOpenLogsContext: handleClearActiveLog,
});
const { draggedColumns, onColumnOrderChange } = useDragColumns<
Record<string, unknown>
>(LOCALSTORAGE.LOGS_LIST_COLUMNS);
const tableColumns = useMemo(
() => getDraggedColumns<Record<string, unknown>>(columns, draggedColumns),
[columns, draggedColumns],
);
const scrollToLog = useCallback(
(logId: string): void => {
const logIndex = logs.findIndex((log) => log.id === logId);
if (logIndex !== -1 && scrollToIndexRef.current) {
scrollToIndexRef.current(logIndex, { align: 'center' });
}
},
[logs],
);
useEffect(() => {
if (activeLogId) {
scrollToLog(activeLogId);
}
}, [activeLogId]);
const args = {
columns,
tableId: 'virtualized-infinite-reorder-resize',
enableSorting: false,
enableFiltering: false,
enableGlobalFilter: false,
enableColumnReordering: true,
enableColumnResizing: true,
enableColumnPinning: false,
enableRowSelection: false,
enablePagination: false,
showHeaders: true,
defaultColumnWidth: 180,
minColumnWidth: 80,
maxColumnWidth: 480,
// Virtualization + Infinite Scroll
enableVirtualization: true,
estimateRowSize: 56,
overscan: 50,
rowHeight: 56,
enableInfiniteScroll: true,
enableScrollRestoration: false,
fixedHeight: isFrequencyChartVisible ? 560 : 760,
enableDynamicRowHeight: false,
};
const selectedColumns = useMemo(
() =>
tableColumns.map((field) => ({
id: field.key?.toString().toLowerCase().replace(/\./g, '_'), // IMP - Replace dots with underscores as reordering does not work well for accessorKey with dots
// accessorKey: field.name,
accessorFn: (row: Record<string, string>): string =>
row[field.key as string] as string,
header: field.title as string,
// eslint-disable-next-line sonarjs/no-duplicate-string
size: field.key === 'state-indicator' ? 4 : 180,
minSize: field.key === 'state-indicator' ? 4 : 120,
maxSize: field.key === 'state-indicator' ? 4 : 1080,
pin: field.key === 'state-indicator' ? 'left' : 'none',
// eslint-disable-next-line react/no-unstable-nested-components
cell: ({
row,
getValue,
}: {
row: Row<Record<string, string>>;
getValue: () => string | JSX.Element;
}): string | JSX.Element => {
if (field.key === 'state-indicator') {
const type = getLogIndicatorTypeForTable(row.original);
const fontSize = options.fontSize as FontSize;
return <LogStateIndicator type={type} fontSize={fontSize} />;
}
return (
<div
className={`table-cell-content ${
row.original.id === activeLog?.id ? 'active-log' : ''
}`}
>
{getValue()}
</div>
);
},
})),
[tableColumns, options.fontSize, activeLog?.id],
);
const handleColumnOrderChange = (newColumns: ColumnDef<any>[]): void => {
if (isEmpty(newColumns) || isEqual(newColumns, selectedColumns)) return;
const formattedColumns = newColumns.map((column) => ({
id: column.id,
header: column.header,
size: column.size,
minSize: column.minSize,
maxSize: column.maxSize,
key: column.id,
title: column.header as string,
dataIndex: column.id,
}));
onColumnOrderChange(formattedColumns);
};
const handleRowClick = (row: Row<Record<string, unknown>>): void => {
const currentLog = logs.find(({ id }) => id === row.original.id);
handleSetActiveLog(currentLog as ILog);
};
return (
<div
className={`logs-list-table-view-container ${
options.fontSize as FontSize
} max-lines-${options.maxLinesPerRow as number}`}
data-max-lines-per-row={options.maxLinesPerRow}
data-font-size={options.fontSize}
>
<DataTable
// eslint-disable-next-line react/jsx-props-no-spreading
{...args}
columns={selectedColumns as ColumnDef<Record<string, string>, unknown>[]}
data={dataSource}
hasMore
onLoadMore={onLoadMore}
loadingMore={isLoading || isFetching}
onColumnOrderChange={handleColumnOrderChange}
onRowClick={handleRowClick}
scrollToIndexRef={scrollToIndexRef}
/>
{activeLog && (
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={handleClearActiveLog}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleAddToQuery}
onGroupByAttribute={handleGroupByAttribute}
/>
)}
</div>
);
}
export default ColumnView;

View File

@@ -11,5 +11,4 @@ export type LogsExplorerListProps = {
isError: boolean;
error?: Error | APIError;
isFilterApplied: boolean;
isFrequencyChartVisible: boolean;
};

View File

@@ -9,362 +9,4 @@
letter-spacing: -0.005em;
text-align: left;
min-height: 500px;
.logs-list-table-view-container {
.data-table-container {
border: none !important;
}
table {
thead {
tr {
th {
font-family: 'Space Mono', monospace;
font-size: 12px;
color: white !important;
.cursor-col-resize {
width: 2px !important;
cursor: col-resize !important;
opacity: 0.5 !important;
&:hover {
opacity: 1 !important;
}
}
}
td {
font-family: 'Space Mono', monospace;
font-size: 12px;
}
border-bottom: 1px solid var(--bg-slate-400) !important;
}
border-bottom: 1px solid var(--bg-slate-400) !important;
background-color: var(--bg-ink-400) !important;
tr {
&:hover {
background-color: var(--bg-ink-400) !important;
}
}
}
tbody {
tr {
td {
font-family: 'Space Mono', monospace;
font-size: 12px;
padding: 4px !important;
white-space: pre-wrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
&:hover {
background-color: var(--bg-slate-500) !important;
}
border-bottom: 1px solid var(--bg-slate-400) !important;
}
tr:has(.active-log) {
background-color: rgba(
78,
116,
248,
0.5
) !important; // same as bg-robin-500
color: var(--text-vanilla-100) !important;
}
}
thead {
z-index: 0 !important;
}
.log-state-indicator {
padding-left: 0 !important;
.line {
margin: 0 0 !important;
}
}
}
.sticky-header-table-container {
&::-webkit-scrollbar {
width: 0.4rem;
height: 0.4rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
&.small {
tbody {
tr {
td {
.table-cell-content {
font-size: 12px !important;
}
}
}
}
}
&.medium {
tbody {
tr {
td {
.table-cell-content {
font-size: 14px !important;
}
}
}
}
}
&.large {
tbody {
tr {
td {
.table-cell-content {
font-size: 16px !important;
}
}
}
}
}
&.max-lines-1 {
tbody {
tr {
td {
.table-cell-content {
font-size: 12px !important;
}
}
}
}
}
&[data-max-lines-per-row='1'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='2'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='3'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='4'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='5'] {
tbody {
tr {
td {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 5;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
&[data-max-lines-per-row='6'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 6;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='7'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 7;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='8'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 8;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='9'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 9;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
&[data-max-lines-per-row='10'] {
tbody {
tr {
td {
.table-cell-content {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 10;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
}
}
.lightMode {
.logs-list-view-container {
.logs-list-table-view-container {
table {
thead {
tr {
th {
color: var(--text-ink-500) !important;
}
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
border-bottom: 1px solid var(--bg-vanilla-300) !important;
background-color: var(--bg-vanilla-100) !important;
tr {
&:hover {
background-color: var(--bg-vanilla-300) !important;
}
}
}
tbody {
tr {
&:hover {
background-color: var(--bg-vanilla-300) !important;
}
border-bottom: 1px solid var(--bg-vanilla-300) !important;
}
}
}
.sticky-header-table-container {
&::-webkit-scrollbar-thumb {
background: var(--bg-vanilla-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-vanilla-100);
}
}
}
}
}

View File

@@ -27,7 +27,7 @@ import { ILog } from 'types/api/logs/log';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import NoLogs from '../NoLogs/NoLogs';
import ColumnView from './ColumnView/ColumnView';
import InfinityTableView from './InfinityTableView';
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
import { InfinityWrapperStyled } from './styles';
import {
@@ -48,7 +48,6 @@ function LogsExplorerList({
isError,
error,
isFilterApplied,
isFrequencyChartVisible,
}: LogsExplorerListProps): JSX.Element {
const ref = useRef<VirtuosoHandle>(null);
@@ -91,7 +90,6 @@ function LogsExplorerList({
});
}
}, [isLoading, isFetching, isError, logs.length]);
const getItemContent = useCallback(
(_: number, log: ILog): JSX.Element => {
if (options.format === 'raw') {
@@ -130,6 +128,75 @@ function LogsExplorerList({
],
);
const renderContent = useMemo(() => {
const components = isLoading
? {
Footer,
}
: {};
if (options.format === 'table') {
return (
<InfinityTableView
ref={ref}
isLoading={isLoading}
tableViewProps={{
logs,
fields: selectedFields,
linesPerRow: options.maxLines,
fontSize: options.fontSize,
appendTo: 'end',
activeLogIndex,
}}
infitiyTableProps={{ onEndReached }}
/>
);
}
function getMarginTop(): string {
switch (options.fontSize) {
case FontSize.SMALL:
return '10px';
case FontSize.MEDIUM:
return '12px';
case FontSize.LARGE:
return '15px';
default:
return '15px';
}
}
return (
<Card
style={{ width: '100%', marginTop: getMarginTop() }}
bodyStyle={CARD_BODY_STYLE}
>
<OverlayScrollbar isVirtuoso>
<Virtuoso
key={activeLogIndex || 'logs-virtuoso'}
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
endReached={onEndReached}
totalCount={logs.length}
itemContent={getItemContent}
components={components}
/>
</OverlayScrollbar>
</Card>
);
}, [
isLoading,
options.format,
options.maxLines,
options.fontSize,
activeLogIndex,
logs,
onEndReached,
getItemContent,
selectedFields,
]);
const isTraceToLogsNavigation = useMemo(() => {
if (!currentStagedQueryData) return false;
return isTraceToLogsQuery(currentStagedQueryData);
@@ -169,83 +236,6 @@ function LogsExplorerList({
return getEmptyLogsListConfig(handleClearFilters);
}, [isTraceToLogsNavigation, handleClearFilters]);
const handleLoadMore = useCallback(() => {
if (isLoading || isFetching) return;
onEndReached(logs.length);
}, [isLoading, isFetching, onEndReached, logs.length]);
const renderContent = useMemo(() => {
const components = isLoading
? {
Footer,
}
: {};
if (options.format === 'table') {
return (
<ColumnView
logs={logs}
onLoadMore={handleLoadMore}
selectedFields={selectedFields}
isLoading={isLoading}
isFetching={isFetching}
options={{
maxLinesPerRow: options.maxLines,
fontSize: options.fontSize,
}}
isFrequencyChartVisible={isFrequencyChartVisible}
/>
);
}
function getMarginTop(): string {
switch (options.fontSize) {
case FontSize.SMALL:
return '10px';
case FontSize.MEDIUM:
return '12px';
case FontSize.LARGE:
return '15px';
default:
return '15px';
}
}
return (
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
<Card
style={{ width: '100%', marginTop: getMarginTop() }}
bodyStyle={CARD_BODY_STYLE}
>
<OverlayScrollbar isVirtuoso>
<Virtuoso
key={activeLogIndex || 'logs-virtuoso'}
ref={ref}
initialTopMostItemIndex={activeLogIndex !== -1 ? activeLogIndex : 0}
data={logs}
endReached={onEndReached}
totalCount={logs.length}
itemContent={getItemContent}
components={components}
/>
</OverlayScrollbar>
</Card>
</InfinityWrapperStyled>
);
}, [
isLoading,
activeLogIndex,
handleLoadMore,
isFetching,
logs,
onEndReached,
getItemContent,
selectedFields,
isFrequencyChartVisible,
options,
]);
return (
<div className="logs-list-view-container">
{(isLoading || (isFetching && logs.length === 0)) && <LogsLoading />}
@@ -274,7 +264,9 @@ function LogsExplorerList({
{!isLoading && !isError && logs.length > 0 && (
<>
{renderContent}
<InfinityWrapperStyled data-testid="logs-list-virtuoso">
{renderContent}
</InfinityWrapperStyled>
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}

View File

@@ -9,7 +9,7 @@
display: flex;
flex-direction: column;
flex: 1;
padding-bottom: 10px;
padding-bottom: 60px;
.views-tabs-container {
padding: 8px 16px;
@@ -216,19 +216,15 @@
background-color: var(--bg-ink-500);
}
.logs-frequency-chart-container {
padding: 0px 8px;
.logs-frequency-chart {
.ant-card-body {
height: 140px;
min-height: 140px;
padding: 0 16px 22px 16px;
font-family: 'Geist Mono';
}
margin-bottom: 0px;
.logs-histogram {
.ant-card-body {
height: 140px;
min-height: 140px;
padding: 0 16px 22px 16px;
font-family: 'Geist Mono';
}
margin-bottom: 0px;
}
}

View File

@@ -2,8 +2,6 @@
import './LogsExplorerViews.styles.scss';
import { Button, Switch, Typography } from 'antd';
import getFromLocalstorage from 'api/browser/localstorage/get';
import setToLocalstorage from 'api/browser/localstorage/set';
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
import logEvent from 'api/common/logEvent';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
@@ -105,13 +103,7 @@ function LogsExplorerViewsContainer({
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const dispatch = useDispatch();
const [showFrequencyChart, setShowFrequencyChart] = useState(false);
useEffect(() => {
const frequencyChart = getFromLocalstorage(LOCALSTORAGE.SHOW_FREQUENCY_CHART);
setShowFrequencyChart(frequencyChart === 'true');
}, []);
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
// this is to respect the panel type present in the URL rather than defaulting it to list always.
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
@@ -680,18 +672,6 @@ function LogsExplorerViewsContainer({
[logs, timezone.value],
);
const handleToggleFrequencyChart = useCallback(() => {
const newShowFrequencyChart = !showFrequencyChart;
// store the value in local storage
setToLocalstorage(
LOCALSTORAGE.SHOW_FREQUENCY_CHART,
newShowFrequencyChart?.toString() || 'false',
);
setShowFrequencyChart(newShowFrequencyChart);
}, [showFrequencyChart]);
return (
<div className="logs-explorer-views-container">
<div className="logs-explorer-views-types">
@@ -705,7 +685,7 @@ function LogsExplorerViewsContainer({
size="small"
checked={showFrequencyChart}
defaultChecked
onChange={handleToggleFrequencyChart}
onChange={(): void => setShowFrequencyChart(!showFrequencyChart)}
/>
</div>
)}
@@ -781,14 +761,12 @@ function LogsExplorerViewsContainer({
</div>
{selectedPanelType === PANEL_TYPES.LIST && showFrequencyChart && (
<div className="logs-frequency-chart-container">
<LogsExplorerChart
className="logs-frequency-chart"
isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData}
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
/>
</div>
<LogsExplorerChart
className="logs-histogram"
isLoading={isFetchingListChartData || isLoadingListChartData}
data={chartData}
isLogsExplorerViews={panelType === PANEL_TYPES.LIST}
/>
)}
<div className="logs-explorer-views-type-content">
@@ -799,12 +777,12 @@ function LogsExplorerViewsContainer({
currentStagedQueryData={listQuery}
logs={logs}
onEndReached={handleEndReached}
isFrequencyChartVisible={showFrequencyChart}
isError={isError}
error={error as APIError}
isFilterApplied={!isEmpty(listQuery?.filters?.items)}
/>
)}
{selectedPanelType === PANEL_TYPES.TIME_SERIES && (
<TimeSeriesView
isLoading={isLoading || isFetching}

View File

@@ -65,6 +65,17 @@ const useOptionsMenu = ({
const [isFocused, setIsFocused] = useState<boolean>(false);
const debouncedSearchText = useDebounce(searchText, 300);
// const initialQueryParams = useMemo(
// () => ({
// searchText: '',
// aggregateAttribute: '',
// tagType: undefined,
// dataSource,
// aggregateOperator,
// }),
// [dataSource, aggregateOperator],
// );
const initialQueryParamsV5: QueryKeyRequestProps = useMemo(
() => ({
signal: dataSource,
@@ -78,6 +89,22 @@ const useOptionsMenu = ({
redirectWithQuery: redirectWithOptionsData,
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
// const initialQueries = useMemo(
// () =>
// initialOptions?.selectColumns?.map((column) => ({
// queryKey: column,
// queryFn: (): Promise<
// SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
// > =>
// getAggregateKeys({
// ...initialQueryParams,
// searchText: column,
// }),
// enabled: !!column && !optionsQuery,
// })) || [],
// [initialOptions?.selectColumns, initialQueryParams, optionsQuery],
// );
const initialQueriesV5 = useMemo(
() =>
initialOptions?.selectColumns?.map((column) => ({

View File

@@ -42,11 +42,11 @@
gap: 8px;
&.active-tab {
background-color: var(--bg-robin-500);
border-bottom: 1px solid var(--bg-robin-500);
background-color: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-ink-500);
&:hover {
background-color: var(--bg-robin-500) !important;
background-color: var(--bg-ink-500) !important;
}
}

View File

@@ -31,6 +31,7 @@ import {
Unplug,
User,
UserPlus,
Video,
} from 'lucide-react';
import { SecondaryMenuItemKey, SidebarItem } from './sideNav.types';
@@ -103,7 +104,6 @@ const menuItems: SidebarItem[] = [
icon: <HardDrive size={16} />,
itemKey: 'services',
},
{
key: ROUTES.LOGS,
label: 'Logs',
@@ -274,6 +274,13 @@ export const defaultMoreMenuItems: SidebarItem[] = [
isBeta: true,
itemKey: 'meter-explorer',
},
{
key: ROUTES.SESSION_RECORDINGS,
label: 'Session Recordings',
icon: <Video size={16} />,
isEnabled: true,
itemKey: 'session-recordings',
},
{
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
label: 'Messaging Queues',

View File

@@ -237,6 +237,8 @@ export const routesToSkip = [
ROUTES.METER,
ROUTES.METER_EXPLORER_VIEWS,
ROUTES.SOMETHING_WENT_WRONG,
ROUTES.SESSION_RECORDINGS,
ROUTES.SESSION_RECORDINGS_DETAIL,
];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@@ -12,7 +12,6 @@ export type UseCopyLogLink = {
isLogsExplorerPage: boolean;
activeLogId: string | null;
onLogCopy: MouseEventHandler<HTMLElement>;
onClearActiveLog: () => void;
};
export type UseActiveLog = {

View File

@@ -1,7 +1,6 @@
import { toast } from '@signozhq/sonner';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import useUrlQueryData from 'hooks/useUrlQueryData';
import {
@@ -22,10 +21,9 @@ import { UseCopyLogLink } from './types';
export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
const urlQuery = useUrlQuery();
const { pathname, search } = useLocation();
const { pathname } = useLocation();
const [, setCopy] = useCopyToClipboard();
const { safeNavigate } = useSafeNavigate();
const { notifications } = useNotifications();
const { queryData: activeLogId } = useUrlQueryData<string | null>(
QueryParams.activeLogId,
@@ -60,19 +58,13 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
setCopy(link);
toast.success('Copied to clipboard', { position: 'top-right' });
notifications.success({
message: 'Copied to clipboard',
});
},
[logId, urlQuery, minTime, maxTime, pathname, setCopy],
[logId, urlQuery, minTime, maxTime, pathname, setCopy, notifications],
);
const onClearActiveLog = useCallback(() => {
const currentUrlQuery = new URLSearchParams(search);
currentUrlQuery.delete(QueryParams.activeLogId);
const newUrl = `${pathname}?${currentUrlQuery.toString()}`;
safeNavigate(newUrl);
}, [pathname, search, safeNavigate]);
useEffect(() => {
if (!isActiveLog) return;
@@ -89,6 +81,5 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
isLogsExplorerPage,
activeLogId,
onLogCopy,
onClearActiveLog,
};
};

View File

@@ -40,13 +40,6 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
[handleRedirectWithDraggedColumns],
);
const onColumnOrderChange = useCallback(
(newColumns: ColumnsType<T>): void => {
handleRedirectWithDraggedColumns(newColumns);
},
[handleRedirectWithDraggedColumns],
);
const redirectWithNewDraggedColumns = useCallback(
async (localStorageColumns: string) => {
let nextDraggedColumns: ColumnsType<T> = [];
@@ -76,7 +69,6 @@ const useDragColumns = <T>(storageKey: LOCALSTORAGE): UseDragColumns<T> => {
return {
draggedColumns,
onDragColumns,
onColumnOrderChange,
};
};

View File

@@ -7,5 +7,4 @@ export type UseDragColumns<T> = {
fromIndex: number,
toIndex: number,
) => void;
onColumnOrderChange: (newColumns: ColumnsType<T>) => void;
};

View File

@@ -105,6 +105,7 @@ const logsQueryServerRequest = (): void =>
describe('Logs Explorer Tests', () => {
test('Logs Explorer default view test without data', async () => {
const {
getByText,
getByRole,
queryByText,
getByTestId,
@@ -122,10 +123,13 @@ describe('Logs Explorer Tests', () => {
</MemoryRouter>,
);
// by default is hidden, toggle the chart and check it's visibility
// check the presence of frequency chart content
expect(getByText(frequencyChartContent)).toBeInTheDocument();
// toggle the chart and check it gets removed from the DOM
const histogramToggle = getByRole('switch');
fireEvent.click(histogramToggle);
expect(queryByText(frequencyChartContent)).toBeInTheDocument();
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
// check the presence of search bar and query builder and absence of clickhouse
const searchView = getByTestId('search-view');
@@ -273,10 +277,10 @@ describe('Logs Explorer Tests', () => {
const histogramToggle = getByRole('switch');
expect(histogramToggle).toBeInTheDocument();
expect(histogramToggle).toBeChecked();
expect(queryByText(frequencyChartContent)).toBeInTheDocument();
// toggle the chart and check it gets removed from the DOM
await fireEvent.click(histogramToggle);
expect(histogramToggle).not.toBeChecked();
expect(queryByText(frequencyChartContent)).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,82 @@
# Session Recordings Page
A minimal, focused page that displays session recordings with just session names and play buttons.
## Features
- **Simple Session List**: Clean table showing only session names and play buttons
- **Direct Access**: Click play button or row to open session recording
- **Minimal UI**: No filters, search, or extra information - just the essentials
- **Responsive Design**: Adapts to different screen sizes
- **Dark/Light Mode Support**: Follows the app's theme system
## Components
### Main Component
- `index.tsx` - Main session recordings page component
### Supporting Files
- `types.ts` - TypeScript interfaces for session recording data
- `styles.scss` - Minimal styling with theme support
- `README.md` - This documentation file
## Data Structure
Each session recording includes:
- Basic identification (ID, session ID)
- User information (name, user agent)
- Timing details (start time, duration)
- Geographic data (country, city)
- Technical details (device, browser, OS)
- Status information (completion status, error flags)
- Recording URL for playback
## Table Columns
1. **Session Name** - Session identifier
2. **Actions** - Play button to open session recording
## Usage
The page automatically loads with mock data for demonstration. In production, replace the mock data with actual API calls to fetch session recordings.
### Table Interaction
- Click on any row to open the session recording
- Use the play button in the Actions column for quick access
- Sort by session name by clicking the column header
- Navigate through pages using the pagination controls
## Styling
The page uses CSS custom properties for theming:
- Dark mode: Uses `--bg-ink-*` and `--bg-slate-*` color variables
- Light mode: Uses `--bg-vanilla-*` color variables
- Accent colors: Uses `--bg-sakura-*` for primary actions and highlights
## Responsive Behavior
- **Desktop**: Clean table layout with proper spacing
- **Tablet**: Responsive table with maintained readability
- **Mobile**: Single-column layout with touch-friendly buttons
## Design Philosophy
The UI follows an extremely minimalist approach:
- Only essential information displayed
- No visual clutter or unnecessary features
- Focus on quick access to session recordings
- Clean, readable table layout
- Consistent with the app's design system
## Future Enhancements
- Session count display
- Basic sorting options
- Export functionality
- Real-time updates for active sessions

View File

@@ -0,0 +1,80 @@
import React, { useEffect, useRef } from 'react';
import Player, { RRwebPlayerOptions } from 'rrweb-player';
import { eventWithTime } from '@rrweb/types';
import 'rrweb-player/dist/style.css'; // Import the styles for the player
interface RRWebPlayerProps {
events: eventWithTime[];
options?: Partial<RRwebPlayerOptions['props']>;
className?: string;
style?: React.CSSProperties;
}
const RRWebPlayer: React.FC<RRWebPlayerProps> = ({
events,
options = {},
className = '',
style = {},
}) => {
const playerRef = useRef<HTMLDivElement>(null);
const playerInstanceRef = useRef<any>(null);
useEffect(() => {
if (!playerRef.current || !events || events.length === 0) {
return;
}
// Clean up previous instance if it exists
if (playerInstanceRef.current) {
// Remove the previous player element
if (playerRef.current) {
playerRef.current.innerHTML = '';
}
}
// Create new player instance using the imported Player
playerInstanceRef.current = new Player({
target: playerRef.current as HTMLElement,
props: {
events,
...options,
},
});
// Cleanup function
return () => {
if (playerInstanceRef.current && playerRef.current) {
playerRef.current.innerHTML = '';
playerInstanceRef.current = null;
}
};
}, [events, options]);
// Cleanup on unmount
useEffect(() => {
return () => {
if (playerInstanceRef.current && playerRef.current) {
playerRef.current.innerHTML = '';
playerInstanceRef.current = null;
}
};
}, []);
if (!events || events.length === 0) {
return (
<div className={`rrweb-player-empty ${className}`} style={style}>
<p>No session events available</p>
</div>
);
}
return (
<div
ref={playerRef}
className={`rrweb-player-container ${className}`}
style={style}
/>
);
};
export default RRWebPlayer;

View File

@@ -0,0 +1,129 @@
import * as Sentry from '@sentry/react';
import { Button, Typography } from 'antd';
import { ArrowLeft } from 'lucide-react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DataSource } from 'types/common/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { SessionRecording } from '../types';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES, initialQueriesMap } from 'constants/queryBuilder';
import RRWebPlayer from '../RRWebPlayer';
import React from 'react';
import './styles.scss';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
const { Title, Text, Paragraph } = Typography;
export default function SessionDetail(): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const { sessionId } = useParams<{ sessionId: string }>();
const history = useHistory();
// Fetch session-related logs data using useGetQueryRange
const {
data: sessionLogsData,
isLoading: isSessionLogsLoading,
} = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.logs,
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: '3d',
params: {
dataSource: DataSource.LOGS,
},
formatForWeb: false,
},
ENTITY_VERSION_V5,
{
queryKey: ['sessionLogs', sessionId],
enabled: !!sessionId && !!stagedQuery,
},
);
console.log({ sessionLogsData });
// Extract body fields from session logs data
const sessionEvents = React.useMemo(() => {
if (!sessionLogsData?.payload?.data?.newResult?.data?.result?.[0]?.list) {
return [];
}
return sessionLogsData.payload.data.newResult.data.result[0].list
.map((row: any) => {
// Try to extract body field from different possible locations
const body =
row.data?.body || row.data?.message || row.data?.log || row.data;
// If body is a string, try to parse it as JSON
if (typeof body === 'string') {
try {
return JSON.parse(body);
} catch {
// If parsing fails, return the string as is
return body;
}
}
return body;
})
.filter(Boolean); // Remove any undefined/null values
}, [sessionLogsData]);
const handleBack = (): void => {
history.push(ROUTES.SESSION_RECORDINGS);
};
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="session-detail-page">
<div className="page-header">
<div className="header-content">
<Button
type="text"
icon={<ArrowLeft size={16} />}
onClick={handleBack}
className="back-button"
>
Back to Sessions
</Button>
<Title level={2} className="page-title">
Session Recording: {sessionId}
</Title>
<Text type="secondary" className="page-description">
Detailed view of session recording and metadata
</Text>
</div>
</div>
<div className="page-content">
{/* Display RRWebPlayer with session events */}
{isSessionLogsLoading ? (
<div>Loading session logs...</div>
) : sessionEvents.length > 0 ? (
<div>
<h3>Session Recording Player</h3>
<div style={{ marginBottom: '16px' }}>
<strong>Total Events: {sessionEvents.length}</strong>
</div>
<RRWebPlayer
events={sessionEvents}
options={{
autoPlay: false,
}}
/>
</div>
) : (
<div>No session events available</div>
)}
</div>
</div>
</Sentry.ErrorBoundary>
);
}

View File

@@ -0,0 +1,306 @@
.session-detail-page {
height: 100%;
background: var(--bg-ink-400);
padding: 0;
.page-header {
background: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-slate-400);
padding: 24px 24px 16px;
margin-bottom: 0;
.header-content {
.back-button {
margin-bottom: 16px;
color: var(--bg-vanilla-400);
padding: 0;
height: auto;
font-size: 14px;
&:hover {
color: var(--bg-vanilla-100);
}
.anticon {
margin-right: 8px;
}
}
.page-title {
margin: 0 0 8px 0;
color: var(--bg-vanilla-100);
font-weight: 600;
font-size: 24px;
line-height: 32px;
}
.page-description {
font-size: 14px;
line-height: 20px;
color: var(--bg-vanilla-400);
margin: 0;
}
}
}
.page-content {
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
.content-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 16px;
.overview-card,
.user-card,
.device-card,
.player-card,
.logs-card {
background: var(--bg-ink-500);
border: 1px solid var(--bg-slate-400);
border-radius: 6px;
.ant-card-head {
background: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-slate-400);
.ant-card-head-title {
color: var(--bg-vanilla-100);
font-weight: 600;
font-size: 16px;
line-height: 24px;
}
}
.ant-card-body {
padding: 16px;
}
.ant-descriptions {
.ant-descriptions-item-label {
color: var(--bg-vanilla-400);
font-weight: 500;
font-size: 12px;
line-height: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ant-descriptions-item-content {
color: var(--bg-vanilla-100);
font-size: 14px;
line-height: 20px;
}
}
.user-agent-text {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
}
.logs-content {
.logs-count {
display: block;
margin-top: 8px;
font-size: 14px;
}
.ant-typography {
margin-bottom: 8px;
}
}
line-height: 16px;
word-break: break-all;
max-width: 100%;
display: block;
}
.ant-tag {
font-size: 12px;
line-height: 16px;
padding: 2px 8px;
border-radius: 4px;
}
.ant-space {
.anticon {
color: var(--bg-vanilla-400);
flex-shrink: 0;
}
}
}
.player-card {
grid-column: span 2;
.player-content {
.player-description {
color: var(--bg-vanilla-400);
font-size: 14px;
line-height: 20px;
margin-bottom: 24px;
text-align: center;
}
.player-actions {
display: flex;
justify-content: center;
.play-button-large {
background: var(--bg-sakura-500);
border-color: var(--bg-sakura-500);
color: var(--bg-ink-500);
height: 48px;
padding: 0 32px;
font-size: 16px;
font-weight: 500;
border-radius: 8px;
&:hover {
background: var(--bg-sakura-600);
border-color: var(--bg-sakura-600);
}
.anticon {
margin-right: 8px;
}
}
}
}
}
}
}
// Light mode overrides
.lightMode {
.session-detail-page {
background: var(--bg-vanilla-100);
.page-header {
background: var(--bg-vanilla-200);
border-bottom: 1px solid var(--bg-slate-300);
.header-content {
.back-button {
color: var(--bg-ink-400);
&:hover {
color: var(--bg-ink-500);
}
}
.page-title {
color: var(--bg-ink-500);
}
.page-description {
color: var(--bg-ink-400);
}
}
}
.page-content {
.content-grid {
.overview-card,
.user-card,
.device-card,
.player-card {
background: var(--bg-vanilla-200);
border: 1px solid var(--bg-slate-300);
.ant-card-head {
background: var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-slate-300);
.ant-card-head-title {
color: var(--bg-ink-500);
}
}
.ant-descriptions {
.ant-descriptions-item-label {
color: var(--bg-ink-400);
}
.ant-descriptions-item-content {
color: var(--bg-ink-500);
}
}
.ant-space {
.anticon {
color: var(--bg-ink-400);
}
}
}
.player-card {
.player-content {
.player-description {
color: var(--bg-ink-400);
}
}
}
}
}
}
}
// Responsive design
@media (max-width: 768px) {
.session-detail-page {
.page-content {
padding: 16px;
.content-grid {
grid-template-columns: 1fr;
gap: 12px;
.player-card {
grid-column: span 1;
}
}
}
}
}
@media (max-width: 480px) {
.session-detail-page {
.page-header {
padding: 16px 16px 12px;
.header-content {
.page-title {
font-size: 20px;
line-height: 28px;
}
}
}
.page-content {
padding: 12px;
.content-grid {
gap: 12px;
.overview-card,
.user-card,
.device-card,
.player-card {
.ant-card-body {
padding: 12px;
}
.ant-descriptions {
.ant-descriptions-item {
margin-bottom: 8px;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,166 @@
import * as Sentry from '@sentry/react';
import { Button, Card, Typography } from 'antd';
import { PlayCircle } from 'lucide-react';
import { useQuery } from 'react-query';
import { useHistory } from 'react-router-dom';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ResizeTable } from 'components/ResizeTable';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DataSource } from 'types/common/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { SessionRecording } from './types';
import { useMemo } from 'react';
import './styles.scss';
const { Title, Text } = Typography;
export default function SessionRecordings(): JSX.Element {
const history = useHistory();
// Use React Query to fetch session attributes
const { data: sessionAttributes, isLoading, error } = useQuery({
queryKey: ['sessionAttributes', 'rum.sessionId'],
queryFn: () =>
getAttributesValues({
aggregateOperator: 'noop',
dataSource: DataSource.LOGS,
aggregateAttribute: '',
attributeKey: 'rum.sessionId',
searchText: '',
filterAttributeKeyDataType: DataTypes.String,
tagType: 'resource',
}),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
});
const getQueryParams = (sessionId: string) => {
const compositeQuery = `compositeQuery=%257B%2522queryType%2522%253A%2522builder%2522%252C%2522builder%2522%253A%257B%2522queryData%2522%253A%255B%257B%2522dataSource%2522%253A%2522logs%2522%252C%2522queryName%2522%253A%2522A%2522%252C%2522aggregateOperator%2522%253A%2522count%2522%252C%2522aggregateAttribute%2522%253A%257B%2522id%2522%253A%2522----%2522%252C%2522dataType%2522%253A%2522%2522%252C%2522key%2522%253A%2522%2522%252C%2522type%2522%253A%2522%2522%257D%252C%2522timeAggregation%2522%253A%2522rate%2522%252C%2522spaceAggregation%2522%253A%2522sum%2522%252C%2522filter%2522%253A%257B%2522expression%2522%253A%2522rum.sessionId%2520%253D%2520%27${sessionId}%27%2520%2522%257D%252C%2522aggregations%2522%253A%255B%257B%2522expression%2522%253A%2522count%28%29%2520%2522%257D%255D%252C%2522functions%2522%253A%255B%255D%252C%2522filters%2522%253A%257B%2522items%2522%253A%255B%255D%252C%2522op%2522%253A%2522AND%2522%257D%252C%2522expression%2522%253A%2522A%2522%252C%2522disabled%2522%253Afalse%252C%2522stepInterval%2522%253Anull%252C%2522having%2522%253A%257B%2522expression%2522%253A%2522%2522%257D%252C%2522limit%2522%253Anull%252C%2522orderBy%2522%253A%255B%255D%252C%2522groupBy%2522%253A%255B%255D%252C%2522legend%2522%253A%2522%2522%252C%2522reduceTo%2522%253A%2522avg%2522%252C%2522source%2522%253A%2522%2522%257D%255D%252C%2522queryFormulas%2522%253A%255B%255D%257D%252C%2522promql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522query%2522%253A%2522%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%257D%255D%252C%2522clickhouse_sql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%252C%2522query%2522%253A%2522%2522%257D%255D%252C%2522id%2522%253A%25226a102b3b-cb6b-4409-9e57-317f9ecb941b%2522%257D&options=%7B%22selectColumns%22%3A%5B%7B%22name%22%3A%22timestamp%22%2C%22signal%22%3A%22logs%22%2C%22fieldContext%22%3A%22log%22%2C%22fieldDataType%22%3A%22%22%2C%22isIndexed%22%3Afalse%7D%2C%7B%22name%22%3A%22body%22%2C%22signal%22%3A%22logs%22%2C%22fieldContext%22%3A%22log%22%2C%22fieldDataType%22%3A%22%22%2C%22isIndexed%22%3Afalse%7D%5D%2C%22maxLines%22%3A2%2C%22format%22%3A%22raw%22%2C%22fontSize%22%3A%22small%22%7D`;
return compositeQuery;
};
// Transform API response to table data
const sessionRecordings: SessionRecording[] = useMemo(() => {
if (
sessionAttributes?.statusCode === 200 &&
sessionAttributes.payload?.stringAttributeValues
) {
return sessionAttributes.payload.stringAttributeValues.map(
(sessionId: string, index: number) => ({
id: String(index + 1),
sessionId,
userName: 'anonymous',
userAgent: 'Mozilla/5.0 (Unknown)',
startTime: new Date().toISOString(), // You can extract timestamp from sessionId if available
duration: 0,
pageViews: 0,
country: 'Unknown',
city: 'Unknown',
device: 'Unknown',
browser: 'Unknown',
os: 'Unknown',
status: 'completed' as const,
hasErrors: false,
recordingUrl: `/session/${sessionId}?${getQueryParams(sessionId)}`,
}),
);
}
return [];
}, [sessionAttributes]);
// Log the response for debugging
if (sessionAttributes && sessionAttributes.statusCode === 200) {
console.log('Session attributes fetched:', sessionAttributes.payload);
}
if (error) {
console.error('Error fetching session attributes:', error);
}
const handleSessionClick = (record: SessionRecording): void => {
history.push(
`/session-recordings/${record.sessionId}?${getQueryParams(
record.sessionId,
)}`,
);
};
const handlePlayClick = (
e: React.MouseEvent,
record: SessionRecording,
): void => {
e.stopPropagation();
history.push(
`/session-recordings/${record.sessionId}?${getQueryParams(
record.sessionId,
)}`,
);
};
const columns = [
{
title: 'Session Name',
dataIndex: 'sessionId',
key: 'sessionId',
width: 200,
render: (sessionId: string) => <Text strong>{sessionId}</Text>,
},
{
title: 'Actions',
key: 'actions',
width: 100,
render: (_: any, record: SessionRecording) => (
<Button
type="primary"
size="small"
icon={<PlayCircle size={14} />}
onClick={(e) => handlePlayClick(e, record)}
className="play-button"
>
Play
</Button>
),
},
];
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="session-recordings-page">
<div className="page-header">
<div className="header-content">
<Title level={2} className="page-title">
Session Recordings
</Title>
<Text type="secondary" className="page-description">
Click play to view session recordings
</Text>
</div>
</div>
<div className="page-content">
<Card className="table-card">
<ResizeTable
columns={columns}
dataSource={sessionRecordings}
rowKey="id"
loading={isLoading}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} of ${total} recordings`,
}}
className="session-recordings-table"
onRow={(record) => ({
onClick: () => handleSessionClick(record),
className: 'clickable-row',
})}
/>
</Card>
</div>
</div>
</Sentry.ErrorBoundary>
);
}

View File

@@ -0,0 +1,221 @@
.session-recordings-page {
height: 100%;
background: var(--bg-ink-400);
padding: 0;
.page-header {
background: var(--bg-ink-500);
border-bottom: 1px solid var(--bg-slate-400);
padding: 24px 24px 16px;
margin-bottom: 0;
.header-content {
.page-title {
margin: 0 0 8px 0;
color: var(--bg-vanilla-100);
font-weight: 600;
font-size: 24px;
line-height: 32px;
}
.page-description {
font-size: 14px;
line-height: 20px;
color: var(--bg-vanilla-400);
margin: 0;
}
}
}
.page-content {
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
.table-card {
background: var(--bg-ink-500);
border: 1px solid var(--bg-slate-400);
border-radius: 6px;
flex: 1;
.ant-card-body {
padding: 16px;
}
.session-recordings-table {
.ant-table {
background: transparent;
.ant-table-thead > tr > th {
background: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-300);
font-weight: 500;
font-size: 12px;
line-height: 16px;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 12px 16px;
}
.ant-table-tbody > tr > td {
background: transparent;
border-bottom: 1px solid var(--bg-slate-400);
padding: 16px;
color: var(--bg-vanilla-100);
}
.ant-table-tbody > tr:hover > td {
background: var(--bg-ink-400);
}
.clickable-row {
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: var(--bg-ink-400);
}
}
}
.ant-pagination {
margin: 16px 0 0 0;
text-align: right;
.ant-pagination-item,
.ant-pagination-prev,
.ant-pagination-next {
background: var(--bg-ink-400);
border: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-400);
&:hover {
border-color: var(--bg-sakura-500);
color: var(--bg-vanilla-100);
}
&.ant-pagination-item-active {
background: var(--bg-sakura-500);
border-color: var(--bg-sakura-500);
color: var(--bg-ink-500);
}
}
.ant-pagination-options {
.ant-select {
.ant-select-selector {
background: var(--bg-ink-400);
border: 1px solid var(--bg-slate-400);
color: var(--bg-vanilla-100);
}
}
}
}
}
}
}
}
// Play button styles
.play-button {
background: var(--bg-sakura-500);
border-color: var(--bg-sakura-500);
color: var(--bg-ink-500);
&:hover {
background: var(--bg-sakura-600);
border-color: var(--bg-sakura-600);
}
.anticon {
margin-right: 4px;
}
}
// Light mode overrides
.lightMode {
.session-recordings-page {
background: var(--bg-vanilla-100);
.page-header {
background: var(--bg-vanilla-200);
border-bottom: 1px solid var(--bg-slate-300);
.header-content {
.page-title {
color: var(--bg-ink-500);
}
.page-description {
color: var(--bg-ink-400);
}
}
}
.page-content {
.table-card {
background: var(--bg-vanilla-200);
border: 1px solid var(--bg-slate-300);
.session-recordings-table {
.ant-table {
.ant-table-thead > tr > th {
background: var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-slate-300);
color: var(--bg-ink-400);
}
.ant-table-tbody > tr > td {
border-bottom: 1px solid var(--bg-slate-300);
color: var(--bg-ink-500);
}
.ant-table-tbody > tr:hover > td {
background: var(--bg-vanilla-300);
}
.clickable-row:hover {
background: var(--bg-vanilla-300);
}
}
.ant-pagination {
.ant-pagination-item,
.ant-pagination-prev,
.ant-pagination-next {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--bg-ink-400);
&:hover {
border-color: var(--bg-sakura-500);
color: var(--bg-ink-500);
}
}
.ant-pagination-options {
.ant-select {
.ant-select-selector {
background: var(--bg-vanilla-300);
border: 1px solid var(--bg-slate-300);
color: var(--bg-ink-500);
}
}
}
}
}
}
}
}
}
// Responsive design
@media (max-width: 768px) {
.session-recordings-page {
.page-content {
padding: 16px;
}
}
}

View File

@@ -0,0 +1,17 @@
export interface SessionRecording {
id: string;
sessionId: string;
userName: string;
userAgent: string;
startTime: string;
duration: number; // seconds
pageViews: number;
country: string;
city: string;
device: string;
browser: string;
os: string;
status: 'completed' | 'in_progress' | 'failed';
hasErrors: boolean;
recordingUrl: string;
}

View File

@@ -108,6 +108,12 @@ const config = {
},
{
test: /\.css$/,
include: /node_modules/,
use: [styleLoader, cssLoader],
},
{
test: /\.css$/,
exclude: /node_modules/,
use: [
styleLoader,
{

View File

@@ -4046,6 +4046,11 @@
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.20.0.tgz#03554155b45d8b529adf635b2f6ad1165d70d8b4"
integrity sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==
"@rrweb/types@2.0.0-alpha.18", "@rrweb/types@^2.0.0-alpha.4":
version "2.0.0-alpha.18"
resolved "https://registry.yarnpkg.com/@rrweb/types/-/types-2.0.0-alpha.18.tgz#e1d9af844cebbf30a2be8808f6cf64f5df3e7f50"
integrity sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==
"@sentry-internal/browser-utils@8.41.0":
version "8.41.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.41.0.tgz#9dc30a8c88aa6e1e542e5acae29ceabd1b377cc4"
@@ -4284,38 +4289,6 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/sonner@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@signozhq/sonner/-/sonner-0.1.0.tgz#1310cc530c60459608246550eb977a1ae27b6ce4"
integrity sha512-P4gc1WdNiX89FZIAhIvR4Bj3sdL7VIpoM80L6otnoeLCZhyFfEOXHGAElvO2z7BuBqJBp1f3pdVDrBQNE6bhyw==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
next-themes "^0.4.6"
sonner "^2.0.7"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/table@0.3.4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@signozhq/table/-/table-0.3.4.tgz#5f243f2977f21fe351207d4ae6db390e400a7933"
integrity sha512-5B3kxYrfNEE9TH1orxAl6CNlESnyNVOgEW08ji9u2kz+khUd0euMCnZPrTBbotRW/INoEwRQxyBPj3auaa3jjQ==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
"@tanstack/react-table" "^8.21.3"
"@tanstack/react-virtual" "^3.13.9"
"@types/lodash-es" "^4.17.12"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lodash-es "^4.17.21"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@sinclair/typebox@^0.25.16":
version "0.25.24"
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz"
@@ -4359,13 +4332,6 @@
dependencies:
"@tanstack/table-core" "8.20.5"
"@tanstack/react-table@^8.21.3":
version "8.21.3"
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b"
integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==
dependencies:
"@tanstack/table-core" "8.21.3"
"@tanstack/react-virtual@3.11.2":
version "3.11.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322"
@@ -4373,33 +4339,16 @@
dependencies:
"@tanstack/virtual-core" "3.11.2"
"@tanstack/react-virtual@^3.13.9":
version "3.13.12"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz#d372dc2783739cc04ec1a728ca8203937687a819"
integrity sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==
dependencies:
"@tanstack/virtual-core" "3.13.12"
"@tanstack/table-core@8.20.5":
version "8.20.5"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
"@tanstack/table-core@8.21.3":
version "8.21.3"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c"
integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==
"@tanstack/virtual-core@3.11.2":
version "3.11.2"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212"
integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==
"@tanstack/virtual-core@3.13.12":
version "3.13.12"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz#1dff176df9cc8f93c78c5e46bcea11079b397578"
integrity sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==
"@testing-library/dom@^8.5.0":
version "8.20.0"
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"
@@ -4487,6 +4436,11 @@
resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz"
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
"@tsconfig/svelte@^1.0.0":
version "1.0.13"
resolved "https://registry.yarnpkg.com/@tsconfig/svelte/-/svelte-1.0.13.tgz#2fa34376627192c0d643ce54964915e2bd3a58e4"
integrity sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==
"@tweenjs/tween.js@18 - 19", "@tweenjs/tween.js@19":
version "19.0.0"
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-19.0.0.tgz"
@@ -4617,6 +4571,11 @@
tapable "^2.0.0"
webpack "^5.1.0"
"@types/css-font-loading-module@0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz#2f98ede46acc0975de85c0b7b0ebe06041d24601"
integrity sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==
"@types/d3-array@3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac"
@@ -4881,13 +4840,6 @@
dependencies:
"@types/node" "*"
"@types/lodash-es@^4.17.12":
version "4.17.12"
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
dependencies:
"@types/lodash" "*"
"@types/lodash-es@^4.17.4":
version "4.17.7"
resolved "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz"
@@ -5787,6 +5739,11 @@
resolved "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz"
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
"@xstate/fsm@^1.4.0":
version "1.6.5"
resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.5.tgz#f599e301997ad7e3c572a0b1ff0696898081bea5"
integrity sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==
"@xstate/react@^3.0.0":
version "3.2.2"
resolved "https://registry.npmjs.org/@xstate/react/-/react-3.2.2.tgz"
@@ -6761,6 +6718,11 @@ balanced-match@^1.0.0:
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-arraybuffer@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
@@ -9460,7 +9422,7 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fflate@^0.4.8:
fflate@^0.4.4, fflate@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
@@ -13218,6 +13180,11 @@ minipass@^4.2.4:
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
mitt@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
mkdirp@^0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
@@ -13389,11 +13356,6 @@ new-array@^1.0.0:
resolved "https://registry.npmjs.org/new-array/-/new-array-1.0.0.tgz"
integrity sha512-K5AyFYbuHZ4e/ti52y7k18q8UHsS78FlRd85w2Fmsd6AkuLipDihPflKC0p3PN5i8II7+uHxo+CtkLiJDfmS5A==
next-themes@^0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6"
integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
ngraph.events@^1.0.0, ngraph.events@^1.2.1:
version "1.2.2"
resolved "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz"
@@ -16057,6 +16019,40 @@ robust-predicates@^3.0.2:
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
rrdom@^0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/rrdom/-/rrdom-0.1.7.tgz#f2f49bfd01b59291bb7b0d981371a5e02a18e2aa"
integrity sha512-ZLd8f14z9pUy2Hk9y636cNv5Y2BMnNEY99wxzW9tD2BLDfe1xFxtLjB4q/xCBYo6HRe0wofzKzjm4JojmpBfFw==
dependencies:
rrweb-snapshot "^2.0.0-alpha.4"
rrweb-player@1.0.0-alpha.4:
version "1.0.0-alpha.4"
resolved "https://registry.yarnpkg.com/rrweb-player/-/rrweb-player-1.0.0-alpha.4.tgz#57576343aaff6c6fb266689fd5d63092be46967c"
integrity sha512-Wlmn9GZ5Fdqa37vd3TzsYdLl/JWEvXNUrLCrYpnOwEgmY409HwVIvvA5aIo7k582LoKgdRCsB87N+f0oWAR0Kg==
dependencies:
"@tsconfig/svelte" "^1.0.0"
rrweb "^2.0.0-alpha.4"
rrweb-snapshot@^2.0.0-alpha.4:
version "2.0.0-alpha.4"
resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.4.tgz#2801bf5946177b9d685a01661a62d9d2e958f174"
integrity sha512-KQ2OtPpXO5jLYqg1OnXS/Hf+EzqnZyP5A+XPqBCjYpj3XIje/Od4gdUwjbFo3cVuWq5Cw5Y1d3/xwgIS7/XpQQ==
rrweb@^2.0.0-alpha.4:
version "2.0.0-alpha.4"
resolved "https://registry.yarnpkg.com/rrweb/-/rrweb-2.0.0-alpha.4.tgz#3c7cf2f1bcf44f7a88dd3fad00ee8d6dd711f258"
integrity sha512-wEHUILbxDPcNwkM3m4qgPgXAiBJyqCbbOHyVoNEVBJzHszWEFYyTbrZqUdeb1EfmTRC2PsumCIkVcomJ/xcOzA==
dependencies:
"@rrweb/types" "^2.0.0-alpha.4"
"@types/css-font-loading-module" "0.0.7"
"@xstate/fsm" "^1.4.0"
base64-arraybuffer "^1.0.1"
fflate "^0.4.4"
mitt "^3.0.0"
rrdom "^0.1.7"
rrweb-snapshot "^2.0.0-alpha.4"
rtl-css-js@^1.14.0, rtl-css-js@^1.16.1:
version "1.16.1"
resolved "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz"
@@ -16498,11 +16494,6 @@ sockjs@^0.3.24:
uuid "^8.3.2"
websocket-driver "^0.7.4"
sonner@^2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/sonner/-/sonner-2.0.7.tgz#810c1487a67ec3370126e0f400dfb9edddc3e4f6"
integrity sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==
sort-asc@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz"

View File

@@ -402,16 +402,6 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
innerSB.OrderBy("duration_nano DESC")
innerSB.SQL("LIMIT 1 BY trace_id")
if query.Limit > 0 {
innerSB.Limit(query.Limit)
} else {
innerSB.Limit(100)
}
if query.Offset > 0 {
innerSB.Offset(query.Offset)
}
innerSQL, innerArgs := innerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
cteFragments = append(cteFragments, fmt.Sprintf("__toe_duration_sorted AS (%s)", innerSQL))
@@ -447,6 +437,10 @@ func (b *traceQueryStatementBuilder) buildTraceQuery(
mainSB.Limit(100)
}
if query.Offset > 0 {
mainSB.Offset(query.Offset)
}
mainSQL, mainArgs := mainSB.BuildWithFlavor(sqlbuilder.ClickHouse)
// combine it all together: WITH … SELECT …

View File

@@ -547,25 +547,8 @@ func TestStatementBuilderTraceQuery(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ?) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, 10},
},
expectedErr: nil,
},
{
name: "List query with mat selected fields with offset",
requestType: qbtypes.RequestTypeTrace,
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
Filter: &qbtypes.Filter{
Expression: "service.name = 'redis-manual'",
},
Limit: 10,
Offset: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? OFFSET ?) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, 10, 10},
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ?), __toe AS (SELECT trace_id FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND true AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __toe_duration_sorted AS (SELECT trace_id, duration_nano, resource_string_service$$name as `service.name`, name FROM signoz_traces.distributed_signoz_index_v3 WHERE parent_span_id = '' AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) SELECT __toe_duration_sorted.`service.name` AS `service.name`, __toe_duration_sorted.name AS `name`, count() AS span_count, __toe_duration_sorted.duration_nano AS `duration_nano`, __toe_duration_sorted.trace_id AS `trace_id` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
},