refactor session replays to be per visit. sessions persist too long resulting in low-quality recordings

This commit is contained in:
Francis Cao
2026-03-03 22:59:25 -08:00
parent b2d45613b5
commit 0e1d6cef6b
26 changed files with 248 additions and 83 deletions
@@ -4,6 +4,7 @@ CREATE TABLE umami.session_replay
replay_id UUID,
website_id UUID,
session_id UUID,
visit_id UUID,
chunk_index UInt32,
events String CODEC(ZSTD(3)),
event_count UInt32,
@@ -13,5 +14,5 @@ CREATE TABLE umami.session_replay
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (replay_id, website_id, session_id, chunk_index)
ORDER BY (replay_id, website_id, session_id, visit_id, chunk_index)
SETTINGS index_granularity = 8192;
+2 -1
View File
@@ -361,6 +361,7 @@ CREATE TABLE umami.session_replay
replay_id UUID,
website_id UUID,
session_id UUID,
visit_id UUID,
chunk_index UInt32,
events String CODEC(ZSTD(3)),
event_count UInt32,
@@ -370,5 +371,5 @@ CREATE TABLE umami.session_replay
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)
ORDER BY (replay_id, website_id, session_id, chunk_index)
ORDER BY (replay_id, website_id, session_id, visit_id, chunk_index)
SETTINGS index_granularity = 8192;
@@ -7,6 +7,7 @@ CREATE TABLE "session_replay" (
"replay_id" UUID NOT NULL,
"website_id" UUID NOT NULL,
"session_id" UUID NOT NULL,
"visit_id" UUID NOT NULL,
"chunk_index" INTEGER NOT NULL,
"events" BYTEA NOT NULL,
"event_count" INTEGER NOT NULL,
@@ -21,5 +22,6 @@ CREATE TABLE "session_replay" (
CREATE INDEX "session_replay_website_id_idx" ON "session_replay"("website_id");
CREATE INDEX "session_replay_session_id_idx" ON "session_replay"("session_id");
CREATE INDEX "session_replay_website_id_session_id_idx" ON "session_replay"("website_id", "session_id");
CREATE INDEX "session_replay_website_id_visit_id_idx" ON "session_replay"("website_id", "visit_id");
CREATE INDEX "session_replay_website_id_created_at_idx" ON "session_replay"("website_id", "created_at");
CREATE INDEX "session_replay_session_id_chunk_index_idx" ON "session_replay"("session_id", "chunk_index");
+2
View File
@@ -385,6 +385,7 @@ model SessionReplay {
id String @id() @map("replay_id") @db.Uuid
websiteId String @map("website_id") @db.Uuid
sessionId String @map("session_id") @db.Uuid
visitId String @map("visit_id") @db.Uuid
chunkIndex Int @map("chunk_index") @db.Integer
events Bytes @map("events")
eventCount Int @map("event_count") @db.Integer
@@ -397,6 +398,7 @@ model SessionReplay {
@@index([websiteId])
@@index([sessionId])
@@index([websiteId, sessionId])
@@index([websiteId, visitId])
@@index([websiteId, createdAt])
@@index([sessionId, chunkIndex])
@@map("session_replay")
+1
View File
@@ -251,6 +251,7 @@
"remove": "Remove",
"remove-member": "Remove member",
"replay": "Replay",
"replay-id": "Replay ID",
"replay-enabled": "Replay enabled",
"replays": "Replays",
"reports": "Reports",
@@ -1,6 +1,6 @@
'use client';
import { Column, Dialog, Modal, type ModalProps } from '@umami/react-zen';
import { ReplayPlayback } from '@/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayback';
import { ReplayPlayback } from '@/app/(main)/websites/[websiteId]/replays/[replayId]/ReplayPlayback';
import { useNavigation } from '@/components/hooks';
export interface ReplayModalProps extends ModalProps {
@@ -33,7 +33,7 @@ export function ReplayModal({ websiteId, ...props }: ReplayModalProps) {
<Dialog variant="sheet">
{({ close }) => (
<Column padding="6">
<ReplayPlayback websiteId={websiteId} sessionId={replay} onClose={close} />
<ReplayPlayback websiteId={websiteId} replayId={replay} onClose={close} />
</Column>
)}
</Dialog>
@@ -22,8 +22,8 @@ export function ReplaysTable({ ...props }: DataTableProps) {
<DataTable {...props}>
<DataColumn id="id" label={t(labels.session)} width="100px">
{(row: any) => (
<Link href={updateParams({ session: row.id })}>
<Avatar seed={row.id} size={32} />
<Link href={updateParams({ session: row.sessionId })}>
<Avatar seed={row.sessionId} size={32} />
</Link>
)}
</DataColumn>
@@ -9,15 +9,15 @@ import { ReplayPlayer } from './ReplayPlayer';
export function ReplayPlayback({
websiteId,
sessionId,
replayId,
onClose,
}: {
websiteId: string;
sessionId: string;
replayId: string;
onClose?: () => void;
}) {
const { data: replay, isLoading, error } = useReplayQuery(websiteId, sessionId);
const { data: session } = useWebsiteSessionQuery(websiteId, sessionId);
const { data: replay, isLoading, error } = useReplayQuery(websiteId, replayId);
const { data: session } = useWebsiteSessionQuery(websiteId, replay?.sessionId);
const { t, labels } = useMessages();
return (
@@ -33,7 +33,7 @@ export function ReplayPlayback({
{session && (
<Row justifyContent="space-between" alignItems="flex-start">
<Row alignItems="center" gap="4">
<Avatar seed={sessionId} size={48} />
<Avatar seed={replay.sessionId} size={48} />
<Column>
<Text weight="bold">{t(labels.replay)}</Text>
<Text color="muted">
@@ -4,11 +4,11 @@ import { ReplayPlayback } from './ReplayPlayback';
export default async function ({
params,
}: {
params: Promise<{ websiteId: string; sessionId: string }>;
params: Promise<{ websiteId: string; replayId: string }>;
}) {
const { websiteId, sessionId } = await params;
const { websiteId, replayId } = await params;
return <ReplayPlayback websiteId={websiteId} sessionId={sessionId} />;
return <ReplayPlayback websiteId={websiteId} replayId={replayId} />;
}
export const metadata: Metadata = {
@@ -10,13 +10,13 @@ import {
TextField,
} from '@umami/react-zen';
import { X } from 'lucide-react';
import { ReplayPlayer } from '@/app/(main)/websites/[websiteId]/replays/[sessionId]/ReplayPlayer';
import { Avatar } from '@/components/common/Avatar';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useMessages, useReplayQuery, useWebsiteSessionQuery } from '@/components/hooks';
import { useMessages, useWebsiteSessionQuery } from '@/components/hooks';
import { SessionActivity } from './SessionActivity';
import { SessionData } from './SessionData';
import { SessionInfo } from './SessionInfo';
import { SessionReplaysDataTable } from './SessionReplaysDataTable';
import { SessionStats } from './SessionStats';
export function SessionProfile({
@@ -29,7 +29,6 @@ export function SessionProfile({
onClose?: () => void;
}) {
const { data, isLoading, error } = useWebsiteSessionQuery(websiteId, sessionId);
const { data: replay } = useReplayQuery(websiteId, sessionId);
const { t, labels } = useMessages();
return (
@@ -65,7 +64,7 @@ export function SessionProfile({
<TabList>
<Tab id="activity">{t(labels.activity)}</Tab>
<Tab id="properties">{t(labels.properties)}</Tab>
{replay?.events?.length > 0 && <Tab id="replay">{t(labels.replay)}</Tab>}
<Tab id="replays">{t(labels.replay)}</Tab>
</TabList>
<TabPanel id="activity">
<SessionActivity
@@ -78,11 +77,9 @@ export function SessionProfile({
<TabPanel id="properties">
<SessionData sessionId={sessionId} websiteId={websiteId} />
</TabPanel>
{replay?.events?.length > 0 && (
<TabPanel id="replay">
<ReplayPlayer events={replay.events} />
</TabPanel>
)}
<TabPanel id="replays">
<SessionReplaysDataTable websiteId={websiteId} sessionId={sessionId} />
</TabPanel>
</Tabs>
</Column>
</Column>
@@ -0,0 +1,45 @@
'use client';
import { Column } from '@umami/react-zen';
import { useState } from 'react';
import { DataGrid } from '@/components/common/DataGrid';
import { useReplayQuery, useSessionReplaysQuery } from '@/components/hooks';
import { ReplayPlayer } from '../replays/[replayId]/ReplayPlayer';
import { SessionReplaysTable } from './SessionReplaysTable';
function InlinePlayer({ websiteId, replayId }: { websiteId: string; replayId: string }) {
const { data: replay } = useReplayQuery(websiteId, replayId);
if (!replay?.events?.length) return null;
return (
<div style={{ padding: '1.5rem 0' }}>
<ReplayPlayer events={replay.events} />
</div>
);
}
export function SessionReplaysDataTable({
websiteId,
sessionId,
}: {
websiteId: string;
sessionId: string;
}) {
const queryResult = useSessionReplaysQuery(websiteId, sessionId);
const [selectedId, setSelectedId] = useState<string | undefined>();
const handlePlay = (id: string) => {
setSelectedId(prev => (prev === id ? undefined : id));
};
return (
<Column>
{selectedId && <InlinePlayer websiteId={websiteId} replayId={selectedId} />}
<DataGrid query={queryResult} allowPaging>
{({ data }) => (
<SessionReplaysTable data={data} onPlay={handlePlay} selectedId={selectedId} />
)}
</DataGrid>
</Column>
);
}
@@ -0,0 +1,44 @@
import { Button, DataColumn, DataTable, type DataTableProps, Icon } from '@umami/react-zen';
import { Play } from 'lucide-react';
import { DateDistance } from '@/components/common/DateDistance';
import { useMessages } from '@/components/hooks';
function formatDuration(ms: number) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
export function SessionReplaysTable({
onPlay,
selectedId,
...props
}: DataTableProps & { onPlay: (id: string) => void; selectedId?: string }) {
const { t, labels } = useMessages();
return (
<DataTable {...props}>
<DataColumn id="id" label={t(labels.replayId)} />
<DataColumn id="duration" label={t(labels.duration)} width="100px">
{(row: any) => formatDuration(row.duration || 0)}
</DataColumn>
<DataColumn id="eventCount" label={t(labels.actions)} width="80px" />
<DataColumn id="createdAt" label={t(labels.recordedAt)} width="140px">
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
</DataColumn>
<DataColumn id="play" label="" width="80px">
{(row: any) => (
<Button
variant={row.id === selectedId ? 'primary' : 'quiet'}
onClick={() => onPlay(row.id)}
>
<Icon>
<Play />
</Icon>
</Button>
)}
</DataColumn>
</DataTable>
);
}
+2 -1
View File
@@ -42,7 +42,7 @@ export async function POST(request: Request) {
return badRequest({ message: 'Invalid session token.' });
}
const { sessionId } = cache;
const { sessionId, visitId } = cache;
// Query directly to avoid stale Redis cache for recordingEnabled
const website = await getWebsite(websiteId);
@@ -80,6 +80,7 @@ export async function POST(request: Request) {
await saveRecording({
websiteId,
sessionId,
visitId,
chunkIndex,
events,
eventCount: events.length,
@@ -1,4 +1,3 @@
import { stitchChunkEvents } from '@/lib/replay';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { canViewWebsite } from '@/permissions';
@@ -6,7 +5,7 @@ import { getReplayChunks } from '@/queries/sql';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string; sessionId: string }> },
{ params }: { params: Promise<{ websiteId: string; replayId: string }> },
) {
const { auth, error } = await parseRequest(request);
@@ -14,19 +13,21 @@ export async function GET(
return error();
}
const { websiteId, sessionId } = await params;
const { websiteId, replayId } = await params;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const chunks = await getReplayChunks(websiteId, sessionId);
const chunks = await getReplayChunks(websiteId, replayId);
const allEvents = stitchChunkEvents(chunks);
const allEvents = chunks.flatMap(chunk => chunk.events);
const sessionId = chunks.length > 0 ? chunks[0].sessionId : null;
const startedAt = chunks.length > 0 ? chunks[0].startedAt : null;
const endedAt = chunks.length > 0 ? chunks[chunks.length - 1].endedAt : null;
return json({
sessionId,
events: allEvents,
startedAt,
endedAt,
@@ -0,0 +1,33 @@
import { getQueryFilters, parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { pagingParams, searchParams, withDateRange } from '@/lib/schema';
import { canViewWebsite } from '@/permissions';
import { getSessionReplays } from '@/queries/sql';
export async function GET(
request: Request,
{ params }: { params: Promise<{ websiteId: string; sessionId: string }> },
) {
const schema = withDateRange({
...pagingParams,
...searchParams,
});
const { auth, query, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { websiteId, sessionId } = await params;
if (!(await canViewWebsite(auth, websiteId))) {
return unauthorized();
}
const filters = await getQueryFilters(query, websiteId);
const data = await getSessionReplays(websiteId, filters, sessionId);
return json(data);
}
+1
View File
@@ -35,6 +35,7 @@ export * from './queries/useSessionActivityQuery';
export * from './queries/useSessionDataPropertiesQuery';
export * from './queries/useSessionDataQuery';
export * from './queries/useSessionDataValuesQuery';
export * from './queries/useSessionReplaysQuery';
export * from './queries/useShareTokenQuery';
export * from './queries/useTeamMembersQuery';
export * from './queries/useTeamQuery';
@@ -1,13 +1,13 @@
import { useApi } from '../useApi';
export function useReplayQuery(websiteId: string, sessionId: string) {
export function useReplayQuery(websiteId: string, replayId: string) {
const { get, useQuery } = useApi();
return useQuery({
queryKey: ['replay', { websiteId, sessionId }],
queryKey: ['replay', { websiteId, replayId }],
queryFn: () => {
return get(`/websites/${websiteId}/replays/${sessionId}`);
return get(`/websites/${websiteId}/replays/${replayId}`);
},
enabled: Boolean(websiteId && sessionId),
enabled: Boolean(websiteId && replayId),
});
}
@@ -0,0 +1,21 @@
import { useApi } from '../useApi';
import { useDateParameters } from '../useDateParameters';
import { usePagedQuery } from '../usePagedQuery';
export function useSessionReplaysQuery(websiteId: string, sessionId: string) {
const { get } = useApi();
const { startAt, endAt, unit, timezone } = useDateParameters();
return usePagedQuery({
queryKey: ['session-replays', { websiteId, sessionId, startAt, endAt, unit, timezone }],
queryFn: pageParams => {
return get(`/websites/${websiteId}/sessions/${sessionId}/replays`, {
startAt,
endAt,
unit,
timezone,
...pageParams,
});
},
});
}
@@ -1,6 +1,6 @@
import { useApi } from '../useApi';
export function useWebsiteSessionQuery(websiteId: string, sessionId: string) {
export function useWebsiteSessionQuery(websiteId: string, sessionId: string | undefined) {
const { get, useQuery } = useApi();
return useQuery({
+1
View File
@@ -359,6 +359,7 @@ export const labels: Record<string, string> = {
sampleSize: 'label.sample-size',
replays: 'label.replays',
replay: 'label.replay',
replayId: 'label.replay-id',
replayEnabled: 'label.replay-enabled',
sampleRate: 'label.sample-rate',
maskLevel: 'label.mask-level',
-17
View File
@@ -1,17 +0,0 @@
import type { ReplayChunk } from '@/queries/sql';
export function stitchChunkEvents(chunks: ReplayChunk[]): any[] {
if (!chunks.length) return [];
const sorted = [...chunks].sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
const result: any[] = [];
let timeOffset = 0;
for (const chunk of sorted) {
const chunkStartMs = chunk.startedAt.getTime();
const events = [...chunk.events].sort((a, b) => a.timestamp - b.timestamp);
for (const event of events) {
result.push({ ...event, timestamp: timeOffset + (event.timestamp - chunkStartMs) });
}
timeOffset += chunk.endedAt.getTime() - chunkStartMs + 1000;
}
return result;
}
+6 -2
View File
@@ -4,6 +4,7 @@ import prisma from '@/lib/prisma';
export interface CreateReplayChunkArgs {
websiteId: string;
sessionId: string;
visitId: string;
chunkIndex: number;
events: Uint8Array;
eventCount: number;
@@ -11,17 +12,18 @@ export interface CreateReplayChunkArgs {
endedAt: Date;
}
export async function getReplayChunks(websiteId: string, sessionId: string) {
export async function getReplayChunks(websiteId: string, visitId: string) {
return prisma.client.sessionReplay.findMany({
where: {
websiteId,
sessionId,
visitId,
},
orderBy: {
chunkIndex: 'asc',
},
select: {
events: true,
sessionId: true,
chunkIndex: true,
eventCount: true,
startedAt: true,
@@ -33,6 +35,7 @@ export async function getReplayChunks(websiteId: string, sessionId: string) {
export async function createReplayChunk({
websiteId,
sessionId,
visitId,
chunkIndex,
events,
eventCount,
@@ -44,6 +47,7 @@ export async function createReplayChunk({
id: uuid(),
websiteId,
sessionId,
visitId,
chunkIndex,
events: new Uint8Array(events) as any,
eventCount,
+21 -12
View File
@@ -6,6 +6,8 @@ import prisma from '@/lib/prisma';
const FUNCTION_NAME = 'getReplayChunks';
export interface ReplayChunk {
sessionId: string;
visitId: string;
events: any[];
chunkIndex: number;
eventCount: number;
@@ -13,20 +15,19 @@ export interface ReplayChunk {
endedAt: Date;
}
export async function getReplayChunks(
websiteId: string,
sessionId: string,
): Promise<ReplayChunk[]> {
export async function getReplayChunks(websiteId: string, visitId: string): Promise<ReplayChunk[]> {
return runQuery({
[PRISMA]: () => relationalQuery(websiteId, sessionId),
[CLICKHOUSE]: () => clickhouseQuery(websiteId, sessionId),
[PRISMA]: () => relationalQuery(websiteId, visitId),
[CLICKHOUSE]: () => clickhouseQuery(websiteId, visitId),
});
}
async function relationalQuery(websiteId: string, sessionId: string): Promise<ReplayChunk[]> {
async function relationalQuery(websiteId: string, visitId: string): Promise<ReplayChunk[]> {
const { rawQuery } = prisma;
const chunks: {
sessionId: string;
visitId: string;
events: Buffer;
chunkIndex: number;
eventCount: number;
@@ -35,6 +36,8 @@ async function relationalQuery(websiteId: string, sessionId: string): Promise<Re
}[] = await rawQuery(
`
select
session_id as "sessionId",
visit_id as "visitId",
events,
chunk_index as "chunkIndex",
event_count as "eventCount",
@@ -42,10 +45,10 @@ async function relationalQuery(websiteId: string, sessionId: string): Promise<Re
ended_at as "endedAt"
from session_replay
where website_id = {{websiteId::uuid}}
and session_id = {{sessionId::uuid}}
and visit_id = {{visitId::uuid}}
order by chunk_index asc
`,
{ websiteId, sessionId },
{ websiteId, visitId },
FUNCTION_NAME,
);
@@ -55,11 +58,13 @@ async function relationalQuery(websiteId: string, sessionId: string): Promise<Re
}));
}
async function clickhouseQuery(websiteId: string, sessionId: string): Promise<ReplayChunk[]> {
async function clickhouseQuery(websiteId: string, visitId: string): Promise<ReplayChunk[]> {
const { rawQuery } = clickhouse;
const results = await rawQuery<
{
sessionId: string;
visitId: string;
events: string;
chunk_index: number;
event_count: number;
@@ -69,6 +74,8 @@ async function clickhouseQuery(websiteId: string, sessionId: string): Promise<Re
>(
`
select
session_id as sessionId,
visit_id as visitId,
events,
chunk_index,
event_count,
@@ -76,14 +83,16 @@ async function clickhouseQuery(websiteId: string, sessionId: string): Promise<Re
ended_at
from session_replay
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
and visit_id = {visitId:UUID}
order by chunk_index asc
`,
{ websiteId, sessionId },
{ websiteId, visitId },
FUNCTION_NAME,
);
return results.map(row => ({
sessionId: row.sessionId,
visitId: row.visitId,
events: JSON.parse(row.events),
chunkIndex: row.chunk_index,
eventCount: row.event_count,
+31 -18
View File
@@ -5,14 +5,16 @@ import type { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionReplays';
export function getSessionReplays(...args: [websiteId: string, filters: QueryFilters]) {
export function getSessionReplays(
...args: [websiteId: string, filters: QueryFilters, sessionId?: string]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
async function relationalQuery(websiteId: string, filters: QueryFilters, sessionId?: string) {
const { pagedRawQuery, parseFilters } = prisma;
const { search } = filters;
const { filterQuery, cohortQuery, queryParams } = parseFilters({
@@ -23,14 +25,19 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
const joinQuery =
filterQuery || cohortQuery
? `join (select *
? `join (select distinct website_id, session_id, visit_id
from website_event
${cohortQuery}
where website_id = {{websiteId::uuid}}
and created_at between {{startDate}} and {{endDate}}) website_event
and created_at between {{startDate}} and {{endDate}}
${filterQuery}) website_event
on website_event.website_id = sr.website_id
and website_event.session_id = sr.session_id`
and website_event.session_id = sr.session_id
and website_event.visit_id = sr.visit_id`
: '';
const sessionFilter = sessionId ? 'and sr.session_id = {{sessionId::uuid}}' : '';
const searchQuery = search
? `and (session.distinct_id ilike {{search}}
or session.city ilike {{search}}
@@ -42,7 +49,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
return pagedRawQuery(
`
select
sr.session_id as "id",
sr.visit_id as "id",
sr.session_id as "sessionId",
sr.website_id as "websiteId",
session.browser,
session.os,
@@ -58,13 +66,13 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
from session_replay sr
join session on session.session_id = sr.session_id
and session.website_id = sr.website_id
${cohortQuery}
${joinQuery}
where sr.website_id = {{websiteId::uuid}}
and sr.created_at between {{startDate}} and {{endDate}}
${filterQuery}
${sessionFilter}
${searchQuery}
group by sr.session_id,
group by sr.visit_id,
sr.session_id,
sr.website_id,
session.browser,
session.os,
@@ -73,13 +81,13 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) {
session.city
order by max(sr.created_at) desc
`,
queryParams,
{ ...queryParams, sessionId },
filters,
FUNCTION_NAME,
);
}
async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, filters: QueryFilters, sessionId?: string) {
const { pagedRawQuery, parseFilters } = clickhouse;
const { search } = filters;
const { queryParams, cohortQuery, filterQuery } = parseFilters({
@@ -87,6 +95,8 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
websiteId,
});
const sessionFilter = sessionId ? 'and session_replay.session_id = {sessionId:UUID}' : '';
const searchQuery = search
? `and ((positionCaseInsensitive(distinct_id, {search:String}) > 0)
or (positionCaseInsensitive(city, {search:String}) > 0)
@@ -98,7 +108,8 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
return pagedRawQuery(
`
select
session_replay.session_id as id,
session_replay.visit_id as id,
session_replay.session_id as sessionId,
session_replay.website_id as websiteId,
website_event.browser,
website_event.os,
@@ -113,22 +124,24 @@ async function clickhouseQuery(websiteId: string, filters: QueryFilters) {
max(session_replay.created_at) as createdAt
from session_replay
join (
select *
select distinct website_id, session_id, visit_id, browser, os, device, country, city
from website_event
${cohortQuery}
where website_id = {websiteId:UUID}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
${searchQuery}
) website_event
on website_event.session_id = session_replay.session_id
and website_event.website_id = session_replay.website_id
${cohortQuery}
and website_event.visit_id = session_replay.visit_id
where session_replay.website_id = {websiteId:UUID}
and session_replay.created_at between {startDate:DateTime64} and {endDate:DateTime64}
${filterQuery}
${searchQuery}
group by session_replay.session_id, session_replay.website_id, website_event.browser, website_event.os, website_event.device, website_event.country, website_event.city
${sessionFilter}
group by session_replay.visit_id, session_replay.session_id, session_replay.website_id, website_event.browser, website_event.os, website_event.device, website_event.country, website_event.city
order by max(created_at) desc
`,
queryParams,
{ ...queryParams, sessionId },
filters,
FUNCTION_NAME,
);
+5
View File
@@ -7,6 +7,7 @@ import prisma from '@/lib/prisma';
export interface SaveRecordingArgs {
websiteId: string;
sessionId: string;
visitId: string;
chunkIndex: number;
events: any[];
eventCount: number;
@@ -24,6 +25,7 @@ export async function saveRecording(args: SaveRecordingArgs) {
async function relationalQuery({
websiteId,
sessionId,
visitId,
chunkIndex,
events,
eventCount,
@@ -37,6 +39,7 @@ async function relationalQuery({
id: uuid(),
websiteId,
sessionId,
visitId,
chunkIndex,
events: compressed as any,
eventCount,
@@ -49,6 +52,7 @@ async function relationalQuery({
async function clickhouseQuery({
websiteId,
sessionId,
visitId,
chunkIndex,
events,
eventCount,
@@ -62,6 +66,7 @@ async function clickhouseQuery({
replay_id: uuid(),
website_id: websiteId,
session_id: sessionId,
visit_id: visitId,
chunk_index: chunkIndex,
events: JSON.stringify(events),
event_count: eventCount,