Rename session recording to session replay across the codebase.
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:
Mike Cao 2026-02-07 00:38:47 -08:00
parent 72b5c658e2
commit 0a3cf7a9ff
34 changed files with 138 additions and 144 deletions

View file

@ -1,10 +1,10 @@
-- AlterTable -- AlterTable
ALTER TABLE "website" ADD COLUMN "recording_enabled" BOOLEAN NOT NULL DEFAULT false; ALTER TABLE "website" ADD COLUMN "replay_enabled" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "website" ADD COLUMN "recording_config" JSONB; ALTER TABLE "website" ADD COLUMN "replay_config" JSONB;
-- CreateTable -- CreateTable
CREATE TABLE "session_recording" ( CREATE TABLE "session_replay" (
"recording_id" UUID NOT NULL, "replay_id" UUID NOT NULL,
"website_id" UUID NOT NULL, "website_id" UUID NOT NULL,
"session_id" UUID NOT NULL, "session_id" UUID NOT NULL,
"chunk_index" INTEGER NOT NULL, "chunk_index" INTEGER NOT NULL,
@ -14,12 +14,12 @@ CREATE TABLE "session_recording" (
"ended_at" TIMESTAMPTZ(6) NOT NULL, "ended_at" TIMESTAMPTZ(6) NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "session_recording_pkey" PRIMARY KEY ("recording_id") CONSTRAINT "session_replay_pkey" PRIMARY KEY ("replay_id")
); );
-- CreateIndex -- CreateIndex
CREATE INDEX "session_recording_website_id_idx" ON "session_recording"("website_id"); CREATE INDEX "session_replay_website_id_idx" ON "session_replay"("website_id");
CREATE INDEX "session_recording_session_id_idx" ON "session_recording"("session_id"); CREATE INDEX "session_replay_session_id_idx" ON "session_replay"("session_id");
CREATE INDEX "session_recording_website_id_session_id_idx" ON "session_recording"("website_id", "session_id"); CREATE INDEX "session_replay_website_id_session_id_idx" ON "session_replay"("website_id", "session_id");
CREATE INDEX "session_recording_website_id_created_at_idx" ON "session_recording"("website_id", "created_at"); CREATE INDEX "session_replay_website_id_created_at_idx" ON "session_replay"("website_id", "created_at");
CREATE INDEX "session_recording_session_id_chunk_index_idx" ON "session_recording"("session_id", "chunk_index"); CREATE INDEX "session_replay_session_id_chunk_index_idx" ON "session_replay"("session_id", "chunk_index");

View file

@ -75,8 +75,8 @@ model Website {
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
recordingEnabled Boolean @default(false) @map("recording_enabled") replayEnabled Boolean @default(false) @map("replay_enabled")
recordingConfig Json? @map("recording_config") replayConfig Json? @map("replay_config")
user User? @relation("user", fields: [userId], references: [id]) user User? @relation("user", fields: [userId], references: [id])
createUser User? @relation("createUser", fields: [createdBy], references: [id]) createUser User? @relation("createUser", fields: [createdBy], references: [id])
@ -86,7 +86,7 @@ model Website {
revenue Revenue[] revenue Revenue[]
segments Segment[] segments Segment[]
sessionData SessionData[] sessionData SessionData[]
sessionRecordings SessionRecording[] sessionReplays SessionReplay[]
@@index([userId]) @@index([userId])
@@index([teamId]) @@index([teamId])
@ -355,8 +355,8 @@ model Share {
@@map("share") @@map("share")
} }
model SessionRecording { model SessionReplay {
id String @id() @map("recording_id") @db.Uuid id String @id() @map("replay_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid websiteId String @map("website_id") @db.Uuid
sessionId String @map("session_id") @db.Uuid sessionId String @map("session_id") @db.Uuid
chunkIndex Int @map("chunk_index") @db.Integer chunkIndex Int @map("chunk_index") @db.Integer
@ -373,5 +373,5 @@ model SessionRecording {
@@index([websiteId, sessionId]) @@index([websiteId, sessionId])
@@index([websiteId, createdAt]) @@index([websiteId, createdAt])
@@index([sessionId, chunkIndex]) @@index([sessionId, chunkIndex])
@@map("session_recording") @@map("session_replay")
} }

View file

@ -15,7 +15,7 @@ export default {
commonjs(), commonjs(),
replace({ replace({
__COLLECT_API_HOST__: process.env.COLLECT_API_HOST || '', __COLLECT_API_HOST__: process.env.COLLECT_API_HOST || '',
__COLLECT_RECORDING_ENDPOINT__: process.env.COLLECT_RECORDING_ENDPOINT || '/api/record', __COLLECT_REPLAY_ENDPOINT__: process.env.COLLECT_REPLAY_ENDPOINT || '/api/record',
delimiters: ['', ''], delimiters: ['', ''],
preventAssignment: true, preventAssignment: true,
}), }),

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -2,14 +2,14 @@
import { Column } from '@umami/react-zen'; import { Column } from '@umami/react-zen';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { Panel } from '@/components/common/Panel'; 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 ( return (
<Column gap="3"> <Column gap="3">
<WebsiteControls websiteId={websiteId} /> <WebsiteControls websiteId={websiteId} />
<Panel> <Panel>
<RecordingsDataTable websiteId={websiteId} /> <ReplaysDataTable websiteId={websiteId} />
</Panel> </Panel>
</Column> </Column>
); );

View file

@ -13,7 +13,7 @@ function formatDuration(ms: number) {
return `${minutes}:${secs.toString().padStart(2, '0')}`; 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 { formatMessage, labels } = useMessages();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
@ -52,7 +52,7 @@ export function RecordingsTable({ websiteId, ...props }: DataTableProps & { webs
</DataColumn> </DataColumn>
<DataColumn id="play" label="" width="80px"> <DataColumn id="play" label="" width="80px">
{(row: any) => ( {(row: any) => (
<Link href={`/websites/${websiteId}/recordings/${row.id}`}> <Link href={`/websites/${websiteId}/replays/${row.id}`}>
<Button variant="quiet"> <Button variant="quiet">
<Icon> <Icon>
<Play /> <Play />

View file

@ -3,42 +3,36 @@ import { Column, Row, Text } from '@umami/react-zen';
import { SessionInfo } from '@/app/(main)/websites/[websiteId]/sessions/SessionInfo'; import { SessionInfo } from '@/app/(main)/websites/[websiteId]/sessions/SessionInfo';
import { Avatar } from '@/components/common/Avatar'; import { Avatar } from '@/components/common/Avatar';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useMessages, useRecordingQuery, useWebsiteSessionQuery } from '@/components/hooks'; import { useMessages, useReplayQuery, useWebsiteSessionQuery } from '@/components/hooks';
import { RecordingPlayer } from './RecordingPlayer'; import { ReplayPlayer } from './ReplayPlayer';
export function RecordingPlayback({ export function ReplayPlayback({ websiteId, sessionId }: { websiteId: string; sessionId: string }) {
websiteId, const { data: replay, isLoading, error } = useReplayQuery(websiteId, sessionId);
sessionId,
}: {
websiteId: string;
sessionId: string;
}) {
const { data: recording, isLoading, error } = useRecordingQuery(websiteId, sessionId);
const { data: session } = useWebsiteSessionQuery(websiteId, sessionId); const { data: session } = useWebsiteSessionQuery(websiteId, sessionId);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
<LoadingPanel <LoadingPanel
data={recording} data={replay}
isLoading={isLoading} isLoading={isLoading}
error={error} error={error}
loadingIcon="spinner" loadingIcon="spinner"
loadingPlacement="absolute" loadingPlacement="absolute"
> >
{recording && ( {replay && (
<Column gap="6"> <Column gap="6">
{session && ( {session && (
<Row alignItems="center" gap="4"> <Row alignItems="center" gap="4">
<Avatar seed={sessionId} size={48} /> <Avatar seed={sessionId} size={48} />
<Column> <Column>
<Text weight="bold">{formatMessage(labels.recording)}</Text> <Text weight="bold">{formatMessage(labels.replay)}</Text>
<Text color="muted"> <Text color="muted">
{recording.eventCount} {formatMessage(labels.events).toLowerCase()} {replay.eventCount} {formatMessage(labels.events).toLowerCase()}
</Text> </Text>
</Column> </Column>
</Row> </Row>
)} )}
<RecordingPlayer events={recording.events} /> <ReplayPlayer events={replay.events} />
{session && <SessionInfo data={session} />} {session && <SessionInfo data={session} />}
</Column> </Column>
)} )}

View file

@ -3,7 +3,7 @@ import { Column } from '@umami/react-zen';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import 'rrweb-player/dist/style.css'; import 'rrweb-player/dist/style.css';
export function RecordingPlayer({ events }: { events: any[] }) { export function ReplayPlayer({ events }: { events: any[] }) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<any>(null); const playerRef = useRef<any>(null);
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
@ -17,14 +17,14 @@ export function RecordingPlayer({ events }: { events: any[] }) {
typeCounts[e.type] = (typeCounts[e.type] || 0) + 1; typeCounts[e.type] = (typeCounts[e.type] || 0) + 1;
}); });
const timestamps = events.map((e: any) => e.timestamp).filter(Boolean); 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( console.log(
'[RecordingPlayer] Time range:', '[ReplayPlayer] Time range:',
timestamps.length timestamps.length
? `${Math.min(...timestamps)} - ${Math.max(...timestamps)} (${Math.max(...timestamps) - Math.min(...timestamps)}ms)` ? `${Math.min(...timestamps)} - ${Math.max(...timestamps)} (${Math.max(...timestamps) - Math.min(...timestamps)}ms)`
: 'no timestamps', : '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 // Dynamically import rrweb-player to avoid SSR issues
import('rrweb-player').then(mod => { import('rrweb-player').then(mod => {

View file

@ -1,5 +1,5 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { RecordingPlayback } from './RecordingPlayback'; import { ReplayPlayback } from './ReplayPlayback';
export default async function ({ export default async function ({
params, params,
@ -8,9 +8,9 @@ export default async function ({
}) { }) {
const { websiteId, sessionId } = await params; const { websiteId, sessionId } = await params;
return <RecordingPlayback websiteId={websiteId} sessionId={sessionId} />; return <ReplayPlayback websiteId={websiteId} sessionId={sessionId} />;
} }
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Recording Playback', title: 'Session Replay',
}; };

View file

@ -1,12 +1,12 @@
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { RecordingsPage } from './RecordingsPage'; import { ReplaysPage } from './ReplaysPage';
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params; const { websiteId } = await params;
return <RecordingsPage websiteId={websiteId} />; return <ReplaysPage websiteId={websiteId} />;
} }
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Recordings', title: 'Replays',
}; };

View file

@ -10,10 +10,10 @@ import {
TextField, TextField,
} from '@umami/react-zen'; } from '@umami/react-zen';
import { X } from 'lucide-react'; 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 { Avatar } from '@/components/common/Avatar';
import { LoadingPanel } from '@/components/common/LoadingPanel'; 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 { SessionActivity } from './SessionActivity';
import { SessionData } from './SessionData'; import { SessionData } from './SessionData';
import { SessionInfo } from './SessionInfo'; import { SessionInfo } from './SessionInfo';
@ -29,7 +29,7 @@ export function SessionProfile({
onClose?: () => void; onClose?: () => void;
}) { }) {
const { data, isLoading, error } = useWebsiteSessionQuery(websiteId, sessionId); const { data, isLoading, error } = useWebsiteSessionQuery(websiteId, sessionId);
const { data: recording } = useRecordingQuery(websiteId, sessionId); const { data: replay } = useReplayQuery(websiteId, sessionId);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
@ -65,8 +65,8 @@ export function SessionProfile({
<TabList> <TabList>
<Tab id="activity">{formatMessage(labels.activity)}</Tab> <Tab id="activity">{formatMessage(labels.activity)}</Tab>
<Tab id="properties">{formatMessage(labels.properties)}</Tab> <Tab id="properties">{formatMessage(labels.properties)}</Tab>
{recording?.events?.length > 0 && ( {replay?.events?.length > 0 && (
<Tab id="recording">{formatMessage(labels.recording)}</Tab> <Tab id="replay">{formatMessage(labels.replay)}</Tab>
)} )}
</TabList> </TabList>
<TabPanel id="activity"> <TabPanel id="activity">
@ -80,9 +80,9 @@ export function SessionProfile({
<TabPanel id="properties"> <TabPanel id="properties">
<SessionData sessionId={sessionId} websiteId={websiteId} /> <SessionData sessionId={sessionId} websiteId={websiteId} />
</TabPanel> </TabPanel>
{recording?.events?.length > 0 && ( {replay?.events?.length > 0 && (
<TabPanel id="recording"> <TabPanel id="replay">
<RecordingPlayer events={recording.events} /> <ReplayPlayer events={replay.events} />
</TabPanel> </TabPanel>
)} )}
</Tabs> </Tabs>

View file

@ -11,7 +11,7 @@ import {
import { useState } from 'react'; import { useState } from 'react';
import { useMessages, useUpdateQuery, useWebsite } from '@/components/hooks'; import { useMessages, useUpdateQuery, useWebsite } from '@/components/hooks';
interface RecordingConfig { interface ReplayConfig {
sampleRate?: number; sampleRate?: number;
maskLevel?: string; maskLevel?: string;
maxDuration?: number; maxDuration?: number;
@ -19,19 +19,19 @@ interface RecordingConfig {
retentionDays?: number; retentionDays?: number;
} }
export function WebsiteRecordingSettings({ websiteId }: { websiteId: string }) { export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
const website = useWebsite(); const website = useWebsite();
const { formatMessage, labels, messages } = useMessages(); const { formatMessage, labels, messages } = useMessages();
const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`); 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) => { const handleSubmit = async (data: any) => {
await mutateAsync( await mutateAsync(
{ {
recordingEnabled: enabled, replayEnabled: enabled,
recordingConfig: { replayConfig: {
sampleRate: parseFloat(data.sampleRate) || 1, sampleRate: parseFloat(data.sampleRate) || 1,
maskLevel: data.maskLevel || 'moderate', maskLevel: data.maskLevel || 'moderate',
maxDuration: parseInt(data.maxDuration, 10) || 600000, maxDuration: parseInt(data.maxDuration, 10) || 600000,
@ -61,9 +61,9 @@ export function WebsiteRecordingSettings({ websiteId }: { websiteId: string }) {
}} }}
> >
<Column gap="4"> <Column gap="4">
<Label>{formatMessage(labels.recordings)}</Label> <Label>{formatMessage(labels.replays)}</Label>
<Switch isSelected={enabled} onChange={setEnabled}> <Switch isSelected={enabled} onChange={setEnabled}>
{formatMessage(labels.recordingEnabled)} {formatMessage(labels.replayEnabled)}
</Switch> </Switch>
{enabled && ( {enabled && (
<> <>

View file

@ -2,7 +2,7 @@ import { Column } from '@umami/react-zen';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { WebsiteData } from './WebsiteData'; import { WebsiteData } from './WebsiteData';
import { WebsiteEditForm } from './WebsiteEditForm'; import { WebsiteEditForm } from './WebsiteEditForm';
import { WebsiteRecordingSettings } from './WebsiteRecordingSettings'; import { WebsiteReplaySettings } from './WebsiteReplaySettings';
import { WebsiteShareForm } from './WebsiteShareForm'; import { WebsiteShareForm } from './WebsiteShareForm';
import { WebsiteTrackingCode } from './WebsiteTrackingCode'; import { WebsiteTrackingCode } from './WebsiteTrackingCode';
@ -16,7 +16,7 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal
<WebsiteTrackingCode websiteId={websiteId} /> <WebsiteTrackingCode websiteId={websiteId} />
</Panel> </Panel>
<Panel> <Panel>
<WebsiteRecordingSettings websiteId={websiteId} /> <WebsiteReplaySettings websiteId={websiteId} />
</Panel> </Panel>
<Panel> <Panel>
<WebsiteShareForm websiteId={websiteId} /> <WebsiteShareForm websiteId={websiteId} />

View file

@ -32,7 +32,7 @@ export function WebsiteTrackingCode({
let code = `<script defer src="${url}" data-website-id="${websiteId}"></script>`; let code = `<script defer src="${url}" data-website-id="${websiteId}"></script>`;
if (website?.recordingEnabled) { if (website?.replayEnabled) {
const recorderUrl = getUrl(RECORDER_NAME); const recorderUrl = getUrl(RECORDER_NAME);
code += `\n<script defer src="${recorderUrl}" data-website-id="${websiteId}"></script>`; code += `\n<script defer src="${recorderUrl}" data-website-id="${websiteId}"></script>`;
} }

View file

@ -8,7 +8,7 @@ import { parseToken } from '@/lib/jwt';
import { parseRequest } from '@/lib/request'; import { parseRequest } from '@/lib/request';
import { badRequest, forbidden, json, serverError } from '@/lib/response'; import { badRequest, forbidden, json, serverError } from '@/lib/response';
import { getWebsite } from '@/queries/prisma'; import { getWebsite } from '@/queries/prisma';
import { saveRecordingChunk } from '@/queries/sql'; import { saveReplayChunk } from '@/queries/sql';
const schema = z.object({ const schema = z.object({
website: z.uuid(), website: z.uuid(),
@ -52,8 +52,8 @@ export async function POST(request: Request) {
return badRequest({ message: 'Website not found.' }); return badRequest({ message: 'Website not found.' });
} }
if (!website.recordingEnabled) { if (!website.replayEnabled) {
return json({ ok: false, reason: 'recording_disabled' }); return json({ ok: false, reason: 'replay_disabled' });
} }
// Client info for bot/IP checks // Client info for bot/IP checks
@ -82,7 +82,7 @@ export async function POST(request: Request) {
// Use timestamp-based chunk index for ordering // Use timestamp-based chunk index for ordering
const chunkIndex = timestamp || Math.floor(Date.now() / 1000); const chunkIndex = timestamp || Math.floor(Date.now() / 1000);
await saveRecordingChunk({ await saveReplayChunk({
websiteId, websiteId,
sessionId, sessionId,
chunkIndex, chunkIndex,

View file

@ -2,7 +2,7 @@ import { gunzipSync } from 'node:zlib';
import { parseRequest } from '@/lib/request'; import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response'; import { json, unauthorized } from '@/lib/response';
import { canViewWebsite } from '@/permissions'; import { canViewWebsite } from '@/permissions';
import { getRecordingChunks } from '@/queries/sql'; import { getReplayChunks } from '@/queries/sql';
export async function GET( export async function GET(
request: Request, request: Request,
@ -20,7 +20,7 @@ export async function GET(
return unauthorized(); return unauthorized();
} }
const chunks = await getRecordingChunks(websiteId, sessionId); const chunks = await getReplayChunks(websiteId, sessionId);
// Decompress and concatenate all chunks // Decompress and concatenate all chunks
const allEvents = chunks.flatMap(chunk => { const allEvents = chunks.flatMap(chunk => {

View file

@ -3,7 +3,7 @@ import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response'; import { json, unauthorized } from '@/lib/response';
import { dateRangeParams, pagingParams, searchParams } from '@/lib/schema'; import { dateRangeParams, pagingParams, searchParams } from '@/lib/schema';
import { canViewWebsite } from '@/permissions'; import { canViewWebsite } from '@/permissions';
import { getSessionRecordings } from '@/queries/sql'; import { getSessionReplays } from '@/queries/sql';
export async function GET( export async function GET(
request: Request, request: Request,
@ -29,7 +29,7 @@ export async function GET(
const filters = await getQueryFilters(query, websiteId); const filters = await getQueryFilters(query, websiteId);
const data = await getSessionRecordings(websiteId, filters); const data = await getSessionReplays(websiteId, filters);
return json(data); return json(data);
} }

View file

@ -42,8 +42,8 @@ export async function POST(
name: z.string().optional(), name: z.string().optional(),
domain: z.string().optional(), domain: z.string().optional(),
shareId: z.string().max(50).nullable().optional(), shareId: z.string().max(50).nullable().optional(),
recordingEnabled: z.boolean().optional(), replayEnabled: z.boolean().optional(),
recordingConfig: z replayConfig: z
.object({ .object({
sampleRate: z.number().min(0).max(1).optional(), sampleRate: z.number().min(0).max(1).optional(),
maskLevel: z.enum(['strict', 'moderate', 'relaxed']).optional(), maskLevel: z.enum(['strict', 'moderate', 'relaxed']).optional(),
@ -62,7 +62,7 @@ export async function POST(
} }
const { websiteId } = await params; const { websiteId } = await params;
const { name, domain, shareId, recordingEnabled, recordingConfig } = body; const { name, domain, shareId, replayEnabled, replayConfig } = body;
if (!(await canUpdateWebsite(auth, websiteId))) { if (!(await canUpdateWebsite(auth, websiteId))) {
return unauthorized(); return unauthorized();
@ -72,8 +72,8 @@ export async function POST(
const website = await updateWebsite(websiteId, { const website = await updateWebsite(websiteId, {
name, name,
domain, domain,
...(recordingEnabled !== undefined && { recordingEnabled }), ...(replayEnabled !== undefined && { replayEnabled }),
...(recordingConfig !== undefined && { recordingConfig }), ...(replayConfig !== undefined && { replayConfig }),
}); });
if (shareId === null) { if (shareId === null) {

View file

@ -25,8 +25,8 @@ export * from './queries/useLoginQuery';
export * from './queries/usePixelQuery'; export * from './queries/usePixelQuery';
export * from './queries/usePixelsQuery'; export * from './queries/usePixelsQuery';
export * from './queries/useRealtimeQuery'; export * from './queries/useRealtimeQuery';
export * from './queries/useRecordingQuery'; export * from './queries/useReplayQuery';
export * from './queries/useRecordingsQuery'; export * from './queries/useReplaysQuery';
export * from './queries/useReportQuery'; export * from './queries/useReportQuery';
export * from './queries/useReportsQuery'; export * from './queries/useReportsQuery';
export * from './queries/useResultQuery'; export * from './queries/useResultQuery';

View file

@ -1,13 +0,0 @@
import { useApi } from '../useApi';
export function useRecordingQuery(websiteId: string, sessionId: string) {
const { get, useQuery } = useApi();
return useQuery({
queryKey: ['recording', { websiteId, sessionId }],
queryFn: () => {
return get(`/websites/${websiteId}/recordings/${sessionId}`);
},
enabled: Boolean(websiteId && sessionId),
});
}

View file

@ -0,0 +1,13 @@
import { useApi } from '../useApi';
export function useReplayQuery(websiteId: string, sessionId: string) {
const { get, useQuery } = useApi();
return useQuery({
queryKey: ['replay', { websiteId, sessionId }],
queryFn: () => {
return get(`/websites/${websiteId}/replays/${sessionId}`);
},
enabled: Boolean(websiteId && sessionId),
});
}

View file

@ -3,15 +3,15 @@ import { useDateParameters } from '../useDateParameters';
import { useModified } from '../useModified'; import { useModified } from '../useModified';
import { usePagedQuery } from '../usePagedQuery'; import { usePagedQuery } from '../usePagedQuery';
export function useRecordingsQuery(websiteId: string, params?: Record<string, string | number>) { export function useReplaysQuery(websiteId: string, params?: Record<string, string | number>) {
const { get } = useApi(); const { get } = useApi();
const { modified } = useModified('recordings'); const { modified } = useModified('replays');
const { startAt, endAt, unit, timezone } = useDateParameters(); const { startAt, endAt, unit, timezone } = useDateParameters();
return usePagedQuery({ return usePagedQuery({
queryKey: ['recordings', { websiteId, modified, startAt, endAt, unit, timezone, ...params }], queryKey: ['replays', { websiteId, modified, startAt, endAt, unit, timezone, ...params }],
queryFn: pageParams => { queryFn: pageParams => {
return get(`/websites/${websiteId}/recordings`, { return get(`/websites/${websiteId}/replays`, {
startAt, startAt,
endAt, endAt,
unit, unit,

View file

@ -96,10 +96,10 @@ export function useWebsiteNavItems(websiteId: string) {
path: renderPath('/retention'), path: renderPath('/retention'),
}, },
{ {
id: 'recordings', id: 'replays',
label: formatMessage(labels.recordings), label: formatMessage(labels.replays),
icon: <Video />, icon: <Video />,
path: renderPath('/recordings'), path: renderPath('/replays'),
}, },
], ],
}, },

View file

@ -378,17 +378,17 @@ export const labels = defineMessages({
support: { id: 'label.support', defaultMessage: 'Support' }, support: { id: 'label.support', defaultMessage: 'Support' },
documentation: { id: 'label.documentation', defaultMessage: 'Documentation' }, documentation: { id: 'label.documentation', defaultMessage: 'Documentation' },
switchAccount: { id: 'label.switch-account', defaultMessage: 'Switch account' }, switchAccount: { id: 'label.switch-account', defaultMessage: 'Switch account' },
recordings: { id: 'label.recordings', defaultMessage: 'Recordings' }, replays: { id: 'label.replays', defaultMessage: 'Replays' },
recording: { id: 'label.recording', defaultMessage: 'Recording' }, replay: { id: 'label.replay', defaultMessage: 'Replay' },
playRecording: { id: 'label.play-recording', defaultMessage: 'Play recording' }, playReplay: { id: 'label.play-replay', defaultMessage: 'Play replay' },
recordingEnabled: { id: 'label.recording-enabled', defaultMessage: 'Recording enabled' }, replayEnabled: { id: 'label.replay-enabled', defaultMessage: 'Replay enabled' },
sampleRate: { id: 'label.sample-rate', defaultMessage: 'Sample rate' }, sampleRate: { id: 'label.sample-rate', defaultMessage: 'Sample rate' },
maskLevel: { id: 'label.mask-level', defaultMessage: 'Mask level' }, maskLevel: { id: 'label.mask-level', defaultMessage: 'Mask level' },
maxDuration: { id: 'label.max-duration', defaultMessage: 'Max duration' }, maxDuration: { id: 'label.max-duration', defaultMessage: 'Max duration' },
retentionDays: { id: 'label.retention-days', defaultMessage: 'Retention days' }, retentionDays: { id: 'label.retention-days', defaultMessage: 'Retention days' },
duration: { id: 'label.duration', defaultMessage: 'Duration' }, duration: { id: 'label.duration', defaultMessage: 'Duration' },
recordedAt: { id: 'label.recorded-at', defaultMessage: 'Recorded at' }, recordedAt: { id: 'label.recorded-at', defaultMessage: 'Recorded at' },
noRecordings: { id: 'label.no-recordings', defaultMessage: 'No recordings' }, noReplays: { id: 'label.no-replays', defaultMessage: 'No replays' },
strict: { id: 'label.strict', defaultMessage: 'Strict' }, strict: { id: 'label.strict', defaultMessage: 'Strict' },
moderate: { id: 'label.moderate', defaultMessage: 'Moderate' }, moderate: { id: 'label.moderate', defaultMessage: 'Moderate' },
relaxed: { id: 'label.relaxed', defaultMessage: 'Relaxed' }, relaxed: { id: 'label.relaxed', defaultMessage: 'Relaxed' },

View file

@ -136,7 +136,7 @@ export async function resetWebsite(websiteId: string) {
return transaction( return transaction(
async tx => { async tx => {
await tx.sessionRecording.deleteMany({ await tx.sessionReplay.deleteMany({
where: { websiteId }, where: { websiteId },
}); });
@ -187,7 +187,7 @@ export async function deleteWebsite(websiteId: string) {
return transaction( return transaction(
async tx => { async tx => {
await tx.sessionRecording.deleteMany({ await tx.sessionReplay.deleteMany({
where: { websiteId }, where: { websiteId },
}); });

View file

@ -22,10 +22,10 @@ export * from './getWeeklyTraffic';
export * from './pageviews/getPageviewExpandedMetrics'; export * from './pageviews/getPageviewExpandedMetrics';
export * from './pageviews/getPageviewMetrics'; export * from './pageviews/getPageviewMetrics';
export * from './pageviews/getPageviewStats'; export * from './pageviews/getPageviewStats';
export * from './recordings/deleteRecordingsByWebsite'; export * from './replays/deleteReplaysByWebsite';
export * from './recordings/getRecordingChunks'; export * from './replays/getReplayChunks';
export * from './recordings/getSessionRecordings'; export * from './replays/getSessionReplays';
export * from './recordings/saveRecordingChunk'; export * from './replays/saveReplayChunk';
export * from './reports/getBreakdown'; export * from './reports/getBreakdown';
export * from './reports/getFunnel'; export * from './reports/getFunnel';
export * from './reports/getJourney'; export * from './reports/getJourney';

View file

@ -1,4 +0,0 @@
export * from './deleteRecordingsByWebsite';
export * from './getRecordingChunks';
export * from './getSessionRecordings';
export * from './saveRecordingChunk';

View file

@ -1,11 +1,11 @@
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
export async function deleteRecordingsByWebsite(websiteId: string) { export async function deleteReplaysByWebsite(websiteId: string) {
return relationalQuery(websiteId); return relationalQuery(websiteId);
} }
async function relationalQuery(websiteId: string) { async function relationalQuery(websiteId: string) {
return prisma.client.sessionRecording.deleteMany({ return prisma.client.sessionReplay.deleteMany({
where: { websiteId }, where: { websiteId },
}); });
} }

View file

@ -1,11 +1,11 @@
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
export async function getRecordingChunks(websiteId: string, sessionId: string) { export async function getReplayChunks(websiteId: string, sessionId: string) {
return relationalQuery(websiteId, sessionId); return relationalQuery(websiteId, sessionId);
} }
async function relationalQuery(websiteId: string, sessionId: string) { async function relationalQuery(websiteId: string, sessionId: string) {
return prisma.client.sessionRecording.findMany({ return prisma.client.sessionReplay.findMany({
where: { where: {
websiteId, websiteId,
sessionId, sessionId,

View file

@ -1,9 +1,9 @@
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types'; import type { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionRecordings'; const FUNCTION_NAME = 'getSessionReplays';
export async function getSessionRecordings(...args: [websiteId: string, filters: QueryFilters]) { export async function getSessionReplays(...args: [websiteId: string, filters: QueryFilters]) {
return relationalQuery(...args); return relationalQuery(...args);
} }
@ -40,12 +40,12 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
session.country, session.country,
session.city, session.city,
sum(sr.event_count) as "eventCount", sum(sr.event_count) as "eventCount",
count(sr.recording_id) as "chunkCount", count(sr.replay_id) as "chunkCount",
min(sr.started_at) as "startedAt", min(sr.started_at) as "startedAt",
max(sr.ended_at) as "endedAt", max(sr.ended_at) as "endedAt",
(extract(epoch from max(sr.ended_at) - min(sr.started_at)) * 1000)::bigint as "duration", (extract(epoch from max(sr.ended_at) - min(sr.started_at)) * 1000)::bigint as "duration",
max(sr.created_at) as "createdAt" max(sr.created_at) as "createdAt"
from session_recording sr from session_replay sr
left join session on session.session_id = sr.session_id left join session on session.session_id = sr.session_id
and session.website_id = sr.website_id and session.website_id = sr.website_id
where sr.website_id = {{websiteId::uuid}} where sr.website_id = {{websiteId::uuid}}

View file

@ -0,0 +1,4 @@
export * from './deleteReplaysByWebsite';
export * from './getReplayChunks';
export * from './getSessionReplays';
export * from './saveReplayChunk';

View file

@ -1,7 +1,7 @@
import { uuid } from '@/lib/crypto'; import { uuid } from '@/lib/crypto';
import prisma from '@/lib/prisma'; import prisma from '@/lib/prisma';
export interface SaveRecordingChunkArgs { export interface SaveReplayChunkArgs {
websiteId: string; websiteId: string;
sessionId: string; sessionId: string;
chunkIndex: number; chunkIndex: number;
@ -11,7 +11,7 @@ export interface SaveRecordingChunkArgs {
endedAt: Date; endedAt: Date;
} }
export async function saveRecordingChunk(args: SaveRecordingChunkArgs) { export async function saveReplayChunk(args: SaveReplayChunkArgs) {
return relationalQuery(args); return relationalQuery(args);
} }
@ -23,8 +23,8 @@ async function relationalQuery({
eventCount, eventCount,
startedAt, startedAt,
endedAt, endedAt,
}: SaveRecordingChunkArgs) { }: SaveReplayChunkArgs) {
return prisma.client.sessionRecording.create({ return prisma.client.sessionReplay.create({
data: { data: {
id: uuid(), id: uuid(),
websiteId, websiteId,

View file

@ -22,7 +22,7 @@ import { record } from 'rrweb';
const host = const host =
hostUrl || '__COLLECT_API_HOST__' || currentScript.src.split('/').slice(0, -1).join('/'); hostUrl || '__COLLECT_API_HOST__' || currentScript.src.split('/').slice(0, -1).join('/');
const endpoint = `${host.replace(/\/$/, '')}__COLLECT_RECORDING_ENDPOINT__`; const endpoint = `${host.replace(/\/$/, '')}__COLLECT_REPLAY_ENDPOINT__`;
const FLUSH_EVENT_COUNT = 50; const FLUSH_EVENT_COUNT = 50;
const FLUSH_INTERVAL = 10000; const FLUSH_INTERVAL = 10000;