mirror of
https://github.com/umami-software/umami.git
synced 2026-02-13 00:55:37 +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,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");
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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 { 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>
|
||||||
);
|
);
|
||||||
|
|
@ -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 />
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
@ -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 => {
|
||||||
|
|
@ -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',
|
||||||
};
|
};
|
||||||
|
|
@ -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',
|
||||||
};
|
};
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
13
src/components/hooks/queries/useReplayQuery.ts
Normal file
13
src/components/hooks/queries/useReplayQuery.ts
Normal 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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' },
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export * from './deleteRecordingsByWebsite';
|
|
||||||
export * from './getRecordingChunks';
|
|
||||||
export * from './getSessionRecordings';
|
|
||||||
export * from './saveRecordingChunk';
|
|
||||||
|
|
@ -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 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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}}
|
||||||
4
src/queries/sql/replays/index.ts
Normal file
4
src/queries/sql/replays/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './deleteReplaysByWebsite';
|
||||||
|
export * from './getReplayChunks';
|
||||||
|
export * from './getSessionReplays';
|
||||||
|
export * from './saveReplayChunk';
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue