mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
feat: added initial session recording changes
This commit is contained in:
parent
8b99ba0f9f
commit
63b9331c78
@ -133,6 +133,7 @@
|
|||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"rehype-raw": "7.0.0",
|
"rehype-raw": "7.0.0",
|
||||||
|
"rrweb-player": "1.0.0-alpha.4",
|
||||||
"stream": "^0.0.2",
|
"stream": "^0.0.2",
|
||||||
"style-loader": "1.3.0",
|
"style-loader": "1.3.0",
|
||||||
"styled-components": "^5.3.11",
|
"styled-components": "^5.3.11",
|
||||||
@ -173,6 +174,7 @@
|
|||||||
"@commitlint/config-conventional": "^16.2.4",
|
"@commitlint/config-conventional": "^16.2.4",
|
||||||
"@faker-js/faker": "9.3.0",
|
"@faker-js/faker": "9.3.0",
|
||||||
"@jest/globals": "^27.5.1",
|
"@jest/globals": "^27.5.1",
|
||||||
|
"@rrweb/types": "2.0.0-alpha.18",
|
||||||
"@testing-library/jest-dom": "5.16.5",
|
"@testing-library/jest-dom": "5.16.5",
|
||||||
"@testing-library/react": "13.4.0",
|
"@testing-library/react": "13.4.0",
|
||||||
"@testing-library/user-event": "14.4.3",
|
"@testing-library/user-event": "14.4.3",
|
||||||
|
|||||||
@ -295,3 +295,15 @@ export const MetricsExplorer = Loadable(
|
|||||||
export const ApiMonitoring = Loadable(
|
export const ApiMonitoring = Loadable(
|
||||||
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
|
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SessionRecordings = Loadable(
|
||||||
|
() =>
|
||||||
|
import(/* webpackChunkName: "SessionRecordings" */ 'pages/SessionRecording'),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SessionRecordingsDetail = Loadable(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "SessionRecordingsDetail" */ 'pages/SessionRecording/SessionDetail'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|||||||
@ -54,6 +54,8 @@ import {
|
|||||||
WorkspaceAccessRestricted,
|
WorkspaceAccessRestricted,
|
||||||
WorkspaceBlocked,
|
WorkspaceBlocked,
|
||||||
WorkspaceSuspended,
|
WorkspaceSuspended,
|
||||||
|
SessionRecordings,
|
||||||
|
SessionRecordingsDetail,
|
||||||
} from './pageComponents';
|
} from './pageComponents';
|
||||||
|
|
||||||
const routes: AppRoutes[] = [
|
const routes: AppRoutes[] = [
|
||||||
@ -450,6 +452,20 @@ const routes: AppRoutes[] = [
|
|||||||
key: 'METER_EXPLORER',
|
key: 'METER_EXPLORER',
|
||||||
isPrivate: true,
|
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,
|
path: ROUTES.METER_EXPLORER_VIEWS,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
|||||||
@ -19,6 +19,8 @@ const ROUTES = {
|
|||||||
GET_STARTED_AZURE_MONITORING: '/get-started/azure-monitoring',
|
GET_STARTED_AZURE_MONITORING: '/get-started/azure-monitoring',
|
||||||
USAGE_EXPLORER: '/usage-explorer',
|
USAGE_EXPLORER: '/usage-explorer',
|
||||||
APPLICATION: '/services',
|
APPLICATION: '/services',
|
||||||
|
SESSION_RECORDINGS: '/session-recordings',
|
||||||
|
SESSION_RECORDINGS_DETAIL: '/session-recordings/:sessionId',
|
||||||
ALL_DASHBOARD: '/dashboard',
|
ALL_DASHBOARD: '/dashboard',
|
||||||
DASHBOARD: '/dashboard/:dashboardId',
|
DASHBOARD: '/dashboard/:dashboardId',
|
||||||
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
|
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import { AnimatePresence } from 'motion/react';
|
|||||||
import * as motion from 'motion/react-client';
|
import * as motion from 'motion/react-client';
|
||||||
import Card from 'periscope/components/Card/Card';
|
import Card from 'periscope/components/Card/Card';
|
||||||
import { useAppContext } from 'providers/App/App';
|
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 { useMutation, useQuery } from 'react-query';
|
||||||
import { UserPreference } from 'types/api/preferences/preference';
|
import { UserPreference } from 'types/api/preferences/preference';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
@ -40,6 +40,9 @@ import HomeChecklist, { ChecklistItem } from './HomeChecklist/HomeChecklist';
|
|||||||
import SavedViews from './SavedViews/SavedViews';
|
import SavedViews from './SavedViews/SavedViews';
|
||||||
import Services from './Services/Services';
|
import Services from './Services/Services';
|
||||||
import StepsProgress from './StepsProgress/StepsProgress';
|
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;
|
const homeInterval = 30 * 60 * 1000;
|
||||||
|
|
||||||
@ -168,6 +171,37 @@ export default function Home(): JSX.Element {
|
|||||||
false,
|
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 processUserPreferences = (userPreferences: UserPreference[]): void => {
|
||||||
const checklistSkipped = Boolean(
|
const checklistSkipped = Boolean(
|
||||||
userPreferences?.find(
|
userPreferences?.find(
|
||||||
@ -311,6 +345,9 @@ export default function Home(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="home-container">
|
<div className="home-container">
|
||||||
|
<p>hello world</p>
|
||||||
|
<RRWebPlayer events={parsedEvents} options={{ autoPlay: false }} />
|
||||||
|
{/*
|
||||||
<div className="sticky-header">
|
<div className="sticky-header">
|
||||||
{showBanner && (
|
{showBanner && (
|
||||||
<div className="home-container-banner">
|
<div className="home-container-banner">
|
||||||
@ -378,7 +415,6 @@ export default function Home(): JSX.Element {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="home-content">
|
<div className="home-content">
|
||||||
<div className="home-left-content">
|
<div className="home-left-content">
|
||||||
<DataSourceInfo
|
<DataSourceInfo
|
||||||
@ -764,7 +800,7 @@ export default function Home(): JSX.Element {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
3724
frontend/src/container/Home/rows.json
Normal file
3724
frontend/src/container/Home/rows.json
Normal file
File diff suppressed because one or more lines are too long
@ -31,6 +31,7 @@ import {
|
|||||||
Unplug,
|
Unplug,
|
||||||
User,
|
User,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
|
Video,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { SecondaryMenuItemKey, SidebarItem } from './sideNav.types';
|
import { SecondaryMenuItemKey, SidebarItem } from './sideNav.types';
|
||||||
@ -103,7 +104,6 @@ const menuItems: SidebarItem[] = [
|
|||||||
icon: <HardDrive size={16} />,
|
icon: <HardDrive size={16} />,
|
||||||
itemKey: 'services',
|
itemKey: 'services',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: ROUTES.LOGS,
|
key: ROUTES.LOGS,
|
||||||
label: 'Logs',
|
label: 'Logs',
|
||||||
@ -274,6 +274,13 @@ export const defaultMoreMenuItems: SidebarItem[] = [
|
|||||||
isBeta: true,
|
isBeta: true,
|
||||||
itemKey: 'meter-explorer',
|
itemKey: 'meter-explorer',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: ROUTES.SESSION_RECORDINGS,
|
||||||
|
label: 'Session Recordings',
|
||||||
|
icon: <Video size={16} />,
|
||||||
|
isEnabled: true,
|
||||||
|
itemKey: 'session-recordings',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
|
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
|
||||||
label: 'Messaging Queues',
|
label: 'Messaging Queues',
|
||||||
|
|||||||
@ -237,6 +237,8 @@ export const routesToSkip = [
|
|||||||
ROUTES.METER,
|
ROUTES.METER,
|
||||||
ROUTES.METER_EXPLORER_VIEWS,
|
ROUTES.METER_EXPLORER_VIEWS,
|
||||||
ROUTES.SOMETHING_WENT_WRONG,
|
ROUTES.SOMETHING_WENT_WRONG,
|
||||||
|
ROUTES.SESSION_RECORDINGS,
|
||||||
|
ROUTES.SESSION_RECORDINGS_DETAIL,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||||
|
|||||||
82
frontend/src/pages/SessionRecording/README.md
Normal file
82
frontend/src/pages/SessionRecording/README.md
Normal 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
|
||||||
80
frontend/src/pages/SessionRecording/RRWebPlayer.tsx
Normal file
80
frontend/src/pages/SessionRecording/RRWebPlayer.tsx
Normal 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;
|
||||||
129
frontend/src/pages/SessionRecording/SessionDetail/index.tsx
Normal file
129
frontend/src/pages/SessionRecording/SessionDetail/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
306
frontend/src/pages/SessionRecording/SessionDetail/styles.scss
Normal file
306
frontend/src/pages/SessionRecording/SessionDetail/styles.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
166
frontend/src/pages/SessionRecording/index.tsx
Normal file
166
frontend/src/pages/SessionRecording/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
221
frontend/src/pages/SessionRecording/styles.scss
Normal file
221
frontend/src/pages/SessionRecording/styles.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
frontend/src/pages/SessionRecording/types.ts
Normal file
17
frontend/src/pages/SessionRecording/types.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -108,6 +108,12 @@ const config = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
|
include: /node_modules/,
|
||||||
|
use: [styleLoader, cssLoader],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
use: [
|
use: [
|
||||||
styleLoader,
|
styleLoader,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4046,6 +4046,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.20.0.tgz#03554155b45d8b529adf635b2f6ad1165d70d8b4"
|
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.20.0.tgz#03554155b45d8b529adf635b2f6ad1165d70d8b4"
|
||||||
integrity sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==
|
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":
|
"@sentry-internal/browser-utils@8.41.0":
|
||||||
version "8.41.0"
|
version "8.41.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.41.0.tgz#9dc30a8c88aa6e1e542e5acae29ceabd1b377cc4"
|
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.41.0.tgz#9dc30a8c88aa6e1e542e5acae29ceabd1b377cc4"
|
||||||
@ -4431,6 +4436,11 @@
|
|||||||
resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz"
|
resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz"
|
||||||
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
|
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":
|
"@tweenjs/tween.js@18 - 19", "@tweenjs/tween.js@19":
|
||||||
version "19.0.0"
|
version "19.0.0"
|
||||||
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-19.0.0.tgz"
|
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-19.0.0.tgz"
|
||||||
@ -4561,6 +4571,11 @@
|
|||||||
tapable "^2.0.0"
|
tapable "^2.0.0"
|
||||||
webpack "^5.1.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":
|
"@types/d3-array@3.0.3":
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac"
|
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac"
|
||||||
@ -5724,6 +5739,11 @@
|
|||||||
resolved "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz"
|
resolved "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz"
|
||||||
integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==
|
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":
|
"@xstate/react@^3.0.0":
|
||||||
version "3.2.2"
|
version "3.2.2"
|
||||||
resolved "https://registry.npmjs.org/@xstate/react/-/react-3.2.2.tgz"
|
resolved "https://registry.npmjs.org/@xstate/react/-/react-3.2.2.tgz"
|
||||||
@ -6698,6 +6718,11 @@ balanced-match@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
||||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
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:
|
base64-js@^1.3.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
|
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
|
||||||
@ -9397,7 +9422,7 @@ fb-watchman@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
bser "2.1.1"
|
bser "2.1.1"
|
||||||
|
|
||||||
fflate@^0.4.8:
|
fflate@^0.4.4, fflate@^0.4.8:
|
||||||
version "0.4.8"
|
version "0.4.8"
|
||||||
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
|
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
|
||||||
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
|
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
|
||||||
@ -13155,6 +13180,11 @@ minipass@^4.2.4:
|
|||||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
|
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
|
||||||
integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
|
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:
|
mkdirp@^0.5.6:
|
||||||
version "0.5.6"
|
version "0.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
|
||||||
@ -15989,6 +16019,40 @@ robust-predicates@^3.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
|
resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771"
|
||||||
integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
|
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:
|
rtl-css-js@^1.14.0, rtl-css-js@^1.16.1:
|
||||||
version "1.16.1"
|
version "1.16.1"
|
||||||
resolved "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz"
|
resolved "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user