mirror of
https://github.com/umami-software/umami.git
synced 2026-02-20 04:25:39 +01:00
Rename session recording to session replay across the codebase.
Some checks failed
Node.js CI / build (push) Has been cancelled
Some checks failed
Node.js CI / build (push) Has been cancelled
Renames all files, components, database schema, API routes, hooks, messages, and build config from "recording" to "replay" terminology. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
72b5c658e2
commit
0a3cf7a9ff
34 changed files with 138 additions and 144 deletions
|
|
@ -1,15 +0,0 @@
|
|||
import { DataGrid } from '@/components/common/DataGrid';
|
||||
import { useRecordingsQuery } from '@/components/hooks';
|
||||
import { RecordingsTable } from './RecordingsTable';
|
||||
|
||||
export function RecordingsDataTable({ websiteId }: { websiteId: string }) {
|
||||
const queryResult = useRecordingsQuery(websiteId);
|
||||
|
||||
return (
|
||||
<DataGrid query={queryResult} allowPaging allowSearch>
|
||||
{({ data }) => {
|
||||
return <RecordingsTable data={data} websiteId={websiteId} />;
|
||||
}}
|
||||
</DataGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { DataGrid } from '@/components/common/DataGrid';
|
||||
import { useReplaysQuery } from '@/components/hooks';
|
||||
import { ReplaysTable } from './ReplaysTable';
|
||||
|
||||
export function ReplaysDataTable({ websiteId }: { websiteId: string }) {
|
||||
const queryResult = useReplaysQuery(websiteId);
|
||||
|
||||
return (
|
||||
<DataGrid query={queryResult} allowPaging allowSearch>
|
||||
{({ data }) => {
|
||||
return <ReplaysTable data={data} websiteId={websiteId} />;
|
||||
}}
|
||||
</DataGrid>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,14 +2,14 @@
|
|||
import { Column } from '@umami/react-zen';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { RecordingsDataTable } from './RecordingsDataTable';
|
||||
import { ReplaysDataTable } from './ReplaysDataTable';
|
||||
|
||||
export function RecordingsPage({ websiteId }: { websiteId: string }) {
|
||||
export function ReplaysPage({ websiteId }: { websiteId: string }) {
|
||||
return (
|
||||
<Column gap="3">
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Panel>
|
||||
<RecordingsDataTable websiteId={websiteId} />
|
||||
<ReplaysDataTable websiteId={websiteId} />
|
||||
</Panel>
|
||||
</Column>
|
||||
);
|
||||
|
|
@ -13,7 +13,7 @@ function formatDuration(ms: number) {
|
|||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function RecordingsTable({ websiteId, ...props }: DataTableProps & { websiteId: string }) {
|
||||
export function ReplaysTable({ websiteId, ...props }: DataTableProps & { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { formatValue } = useFormat();
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ export function RecordingsTable({ websiteId, ...props }: DataTableProps & { webs
|
|||
</DataColumn>
|
||||
<DataColumn id="play" label="" width="80px">
|
||||
{(row: any) => (
|
||||
<Link href={`/websites/${websiteId}/recordings/${row.id}`}>
|
||||
<Link href={`/websites/${websiteId}/replays/${row.id}`}>
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<Play />
|
||||
|
|
@ -3,42 +3,36 @@ import { Column, Row, Text } from '@umami/react-zen';
|
|||
import { SessionInfo } from '@/app/(main)/websites/[websiteId]/sessions/SessionInfo';
|
||||
import { Avatar } from '@/components/common/Avatar';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { useMessages, useRecordingQuery, useWebsiteSessionQuery } from '@/components/hooks';
|
||||
import { RecordingPlayer } from './RecordingPlayer';
|
||||
import { useMessages, useReplayQuery, useWebsiteSessionQuery } from '@/components/hooks';
|
||||
import { ReplayPlayer } from './ReplayPlayer';
|
||||
|
||||
export function RecordingPlayback({
|
||||
websiteId,
|
||||
sessionId,
|
||||
}: {
|
||||
websiteId: string;
|
||||
sessionId: string;
|
||||
}) {
|
||||
const { data: recording, isLoading, error } = useRecordingQuery(websiteId, sessionId);
|
||||
export function ReplayPlayback({ websiteId, sessionId }: { websiteId: string; sessionId: string }) {
|
||||
const { data: replay, isLoading, error } = useReplayQuery(websiteId, sessionId);
|
||||
const { data: session } = useWebsiteSessionQuery(websiteId, sessionId);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<LoadingPanel
|
||||
data={recording}
|
||||
data={replay}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
loadingIcon="spinner"
|
||||
loadingPlacement="absolute"
|
||||
>
|
||||
{recording && (
|
||||
{replay && (
|
||||
<Column gap="6">
|
||||
{session && (
|
||||
<Row alignItems="center" gap="4">
|
||||
<Avatar seed={sessionId} size={48} />
|
||||
<Column>
|
||||
<Text weight="bold">{formatMessage(labels.recording)}</Text>
|
||||
<Text weight="bold">{formatMessage(labels.replay)}</Text>
|
||||
<Text color="muted">
|
||||
{recording.eventCount} {formatMessage(labels.events).toLowerCase()}
|
||||
{replay.eventCount} {formatMessage(labels.events).toLowerCase()}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
)}
|
||||
<RecordingPlayer events={recording.events} />
|
||||
<ReplayPlayer events={replay.events} />
|
||||
{session && <SessionInfo data={session} />}
|
||||
</Column>
|
||||
)}
|
||||
|
|
@ -3,7 +3,7 @@ import { Column } from '@umami/react-zen';
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import 'rrweb-player/dist/style.css';
|
||||
|
||||
export function RecordingPlayer({ events }: { events: any[] }) {
|
||||
export function ReplayPlayer({ events }: { events: any[] }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const playerRef = useRef<any>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
|
@ -17,14 +17,14 @@ export function RecordingPlayer({ events }: { events: any[] }) {
|
|||
typeCounts[e.type] = (typeCounts[e.type] || 0) + 1;
|
||||
});
|
||||
const timestamps = events.map((e: any) => e.timestamp).filter(Boolean);
|
||||
console.log('[RecordingPlayer] Events:', events.length, 'Types:', typeCounts);
|
||||
console.log('[ReplayPlayer] Events:', events.length, 'Types:', typeCounts);
|
||||
console.log(
|
||||
'[RecordingPlayer] Time range:',
|
||||
'[ReplayPlayer] Time range:',
|
||||
timestamps.length
|
||||
? `${Math.min(...timestamps)} - ${Math.max(...timestamps)} (${Math.max(...timestamps) - Math.min(...timestamps)}ms)`
|
||||
: 'no timestamps',
|
||||
);
|
||||
console.log('[RecordingPlayer] First 3 events:', events.slice(0, 3));
|
||||
console.log('[ReplayPlayer] First 3 events:', events.slice(0, 3));
|
||||
|
||||
// Dynamically import rrweb-player to avoid SSR issues
|
||||
import('rrweb-player').then(mod => {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { RecordingPlayback } from './RecordingPlayback';
|
||||
import { ReplayPlayback } from './ReplayPlayback';
|
||||
|
||||
export default async function ({
|
||||
params,
|
||||
|
|
@ -8,9 +8,9 @@ export default async function ({
|
|||
}) {
|
||||
const { websiteId, sessionId } = await params;
|
||||
|
||||
return <RecordingPlayback websiteId={websiteId} sessionId={sessionId} />;
|
||||
return <ReplayPlayback websiteId={websiteId} sessionId={sessionId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Recording Playback',
|
||||
title: 'Session Replay',
|
||||
};
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { RecordingsPage } from './RecordingsPage';
|
||||
import { ReplaysPage } from './ReplaysPage';
|
||||
|
||||
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
|
||||
const { websiteId } = await params;
|
||||
|
||||
return <RecordingsPage websiteId={websiteId} />;
|
||||
return <ReplaysPage websiteId={websiteId} />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Recordings',
|
||||
title: 'Replays',
|
||||
};
|
||||
|
|
@ -10,10 +10,10 @@ import {
|
|||
TextField,
|
||||
} from '@umami/react-zen';
|
||||
import { X } from 'lucide-react';
|
||||
import { RecordingPlayer } from '@/app/(main)/websites/[websiteId]/recordings/[sessionId]/RecordingPlayer';
|
||||
import { ReplayPlayer } from '@/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayer';
|
||||
import { Avatar } from '@/components/common/Avatar';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { useMessages, useRecordingQuery, useWebsiteSessionQuery } from '@/components/hooks';
|
||||
import { useMessages, useReplayQuery, useWebsiteSessionQuery } from '@/components/hooks';
|
||||
import { SessionActivity } from './SessionActivity';
|
||||
import { SessionData } from './SessionData';
|
||||
import { SessionInfo } from './SessionInfo';
|
||||
|
|
@ -29,7 +29,7 @@ export function SessionProfile({
|
|||
onClose?: () => void;
|
||||
}) {
|
||||
const { data, isLoading, error } = useWebsiteSessionQuery(websiteId, sessionId);
|
||||
const { data: recording } = useRecordingQuery(websiteId, sessionId);
|
||||
const { data: replay } = useReplayQuery(websiteId, sessionId);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
|
|
@ -65,8 +65,8 @@ export function SessionProfile({
|
|||
<TabList>
|
||||
<Tab id="activity">{formatMessage(labels.activity)}</Tab>
|
||||
<Tab id="properties">{formatMessage(labels.properties)}</Tab>
|
||||
{recording?.events?.length > 0 && (
|
||||
<Tab id="recording">{formatMessage(labels.recording)}</Tab>
|
||||
{replay?.events?.length > 0 && (
|
||||
<Tab id="replay">{formatMessage(labels.replay)}</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
<TabPanel id="activity">
|
||||
|
|
@ -80,9 +80,9 @@ export function SessionProfile({
|
|||
<TabPanel id="properties">
|
||||
<SessionData sessionId={sessionId} websiteId={websiteId} />
|
||||
</TabPanel>
|
||||
{recording?.events?.length > 0 && (
|
||||
<TabPanel id="recording">
|
||||
<RecordingPlayer events={recording.events} />
|
||||
{replay?.events?.length > 0 && (
|
||||
<TabPanel id="replay">
|
||||
<ReplayPlayer events={replay.events} />
|
||||
</TabPanel>
|
||||
)}
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
import { useState } from 'react';
|
||||
import { useMessages, useUpdateQuery, useWebsite } from '@/components/hooks';
|
||||
|
||||
interface RecordingConfig {
|
||||
interface ReplayConfig {
|
||||
sampleRate?: number;
|
||||
maskLevel?: string;
|
||||
maxDuration?: number;
|
||||
|
|
@ -19,19 +19,19 @@ interface RecordingConfig {
|
|||
retentionDays?: number;
|
||||
}
|
||||
|
||||
export function WebsiteRecordingSettings({ websiteId }: { websiteId: string }) {
|
||||
export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
|
||||
const website = useWebsite();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
|
||||
const [enabled, setEnabled] = useState(website?.recordingEnabled ?? false);
|
||||
const [enabled, setEnabled] = useState(website?.replayEnabled ?? false);
|
||||
|
||||
const config = (website?.recordingConfig as RecordingConfig) || {};
|
||||
const config = (website?.replayConfig as ReplayConfig) || {};
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
recordingEnabled: enabled,
|
||||
recordingConfig: {
|
||||
replayEnabled: enabled,
|
||||
replayConfig: {
|
||||
sampleRate: parseFloat(data.sampleRate) || 1,
|
||||
maskLevel: data.maskLevel || 'moderate',
|
||||
maxDuration: parseInt(data.maxDuration, 10) || 600000,
|
||||
|
|
@ -61,9 +61,9 @@ export function WebsiteRecordingSettings({ websiteId }: { websiteId: string }) {
|
|||
}}
|
||||
>
|
||||
<Column gap="4">
|
||||
<Label>{formatMessage(labels.recordings)}</Label>
|
||||
<Label>{formatMessage(labels.replays)}</Label>
|
||||
<Switch isSelected={enabled} onChange={setEnabled}>
|
||||
{formatMessage(labels.recordingEnabled)}
|
||||
{formatMessage(labels.replayEnabled)}
|
||||
</Switch>
|
||||
{enabled && (
|
||||
<>
|
||||
|
|
@ -2,7 +2,7 @@ import { Column } from '@umami/react-zen';
|
|||
import { Panel } from '@/components/common/Panel';
|
||||
import { WebsiteData } from './WebsiteData';
|
||||
import { WebsiteEditForm } from './WebsiteEditForm';
|
||||
import { WebsiteRecordingSettings } from './WebsiteRecordingSettings';
|
||||
import { WebsiteReplaySettings } from './WebsiteReplaySettings';
|
||||
import { WebsiteShareForm } from './WebsiteShareForm';
|
||||
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal
|
|||
<WebsiteTrackingCode websiteId={websiteId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<WebsiteRecordingSettings websiteId={websiteId} />
|
||||
<WebsiteReplaySettings websiteId={websiteId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<WebsiteShareForm websiteId={websiteId} />
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export function WebsiteTrackingCode({
|
|||
|
||||
let code = `<script defer src="${url}" data-website-id="${websiteId}"></script>`;
|
||||
|
||||
if (website?.recordingEnabled) {
|
||||
if (website?.replayEnabled) {
|
||||
const recorderUrl = getUrl(RECORDER_NAME);
|
||||
code += `\n<script defer src="${recorderUrl}" data-website-id="${websiteId}"></script>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { parseToken } from '@/lib/jwt';
|
|||
import { parseRequest } from '@/lib/request';
|
||||
import { badRequest, forbidden, json, serverError } from '@/lib/response';
|
||||
import { getWebsite } from '@/queries/prisma';
|
||||
import { saveRecordingChunk } from '@/queries/sql';
|
||||
import { saveReplayChunk } from '@/queries/sql';
|
||||
|
||||
const schema = z.object({
|
||||
website: z.uuid(),
|
||||
|
|
@ -52,8 +52,8 @@ export async function POST(request: Request) {
|
|||
return badRequest({ message: 'Website not found.' });
|
||||
}
|
||||
|
||||
if (!website.recordingEnabled) {
|
||||
return json({ ok: false, reason: 'recording_disabled' });
|
||||
if (!website.replayEnabled) {
|
||||
return json({ ok: false, reason: 'replay_disabled' });
|
||||
}
|
||||
|
||||
// Client info for bot/IP checks
|
||||
|
|
@ -82,7 +82,7 @@ export async function POST(request: Request) {
|
|||
// Use timestamp-based chunk index for ordering
|
||||
const chunkIndex = timestamp || Math.floor(Date.now() / 1000);
|
||||
|
||||
await saveRecordingChunk({
|
||||
await saveReplayChunk({
|
||||
websiteId,
|
||||
sessionId,
|
||||
chunkIndex,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { gunzipSync } from 'node:zlib';
|
|||
import { parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { canViewWebsite } from '@/permissions';
|
||||
import { getRecordingChunks } from '@/queries/sql';
|
||||
import { getReplayChunks } from '@/queries/sql';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
|
|
@ -20,7 +20,7 @@ export async function GET(
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const chunks = await getRecordingChunks(websiteId, sessionId);
|
||||
const chunks = await getReplayChunks(websiteId, sessionId);
|
||||
|
||||
// Decompress and concatenate all chunks
|
||||
const allEvents = chunks.flatMap(chunk => {
|
||||
|
|
@ -3,7 +3,7 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
|
|||
import { json, unauthorized } from '@/lib/response';
|
||||
import { dateRangeParams, pagingParams, searchParams } from '@/lib/schema';
|
||||
import { canViewWebsite } from '@/permissions';
|
||||
import { getSessionRecordings } from '@/queries/sql';
|
||||
import { getSessionReplays } from '@/queries/sql';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
|
|
@ -29,7 +29,7 @@ export async function GET(
|
|||
|
||||
const filters = await getQueryFilters(query, websiteId);
|
||||
|
||||
const data = await getSessionRecordings(websiteId, filters);
|
||||
const data = await getSessionReplays(websiteId, filters);
|
||||
|
||||
return json(data);
|
||||
}
|
||||
|
|
@ -42,8 +42,8 @@ export async function POST(
|
|||
name: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
shareId: z.string().max(50).nullable().optional(),
|
||||
recordingEnabled: z.boolean().optional(),
|
||||
recordingConfig: z
|
||||
replayEnabled: z.boolean().optional(),
|
||||
replayConfig: z
|
||||
.object({
|
||||
sampleRate: z.number().min(0).max(1).optional(),
|
||||
maskLevel: z.enum(['strict', 'moderate', 'relaxed']).optional(),
|
||||
|
|
@ -62,7 +62,7 @@ export async function POST(
|
|||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { name, domain, shareId, recordingEnabled, recordingConfig } = body;
|
||||
const { name, domain, shareId, replayEnabled, replayConfig } = body;
|
||||
|
||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
|
|
@ -72,8 +72,8 @@ export async function POST(
|
|||
const website = await updateWebsite(websiteId, {
|
||||
name,
|
||||
domain,
|
||||
...(recordingEnabled !== undefined && { recordingEnabled }),
|
||||
...(recordingConfig !== undefined && { recordingConfig }),
|
||||
...(replayEnabled !== undefined && { replayEnabled }),
|
||||
...(replayConfig !== undefined && { replayConfig }),
|
||||
});
|
||||
|
||||
if (shareId === null) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue