Merge branch 'session-recording' into dev

This commit is contained in:
Mike Cao
2026-02-25 02:32:07 -08:00
35 changed files with 1104 additions and 20 deletions
+4
View File
@@ -24,6 +24,10 @@ export * from './pageviews/getPageviewMetrics';
export * from './pageviews/getPageviewStats';
export * from './performance/getPerformanceStats';
export * from './performance/savePerformance';
export * from './replays/deleteReplaysByWebsite';
export * from './replays/getReplayChunks';
export * from './replays/getSessionReplays';
export * from './replays/saveReplayChunk';
export * from './reports/getBreakdown';
export * from './reports/getFunnel';
export * from './reports/getJourney';
@@ -0,0 +1,11 @@
import prisma from '@/lib/prisma';
export async function deleteReplaysByWebsite(websiteId: string) {
return relationalQuery(websiteId);
}
async function relationalQuery(websiteId: string) {
return prisma.client.sessionReplay.deleteMany({
where: { websiteId },
});
}
@@ -0,0 +1,24 @@
import prisma from '@/lib/prisma';
export async function getReplayChunks(websiteId: string, sessionId: string) {
return relationalQuery(websiteId, sessionId);
}
async function relationalQuery(websiteId: string, sessionId: string) {
return prisma.client.sessionReplay.findMany({
where: {
websiteId,
sessionId,
},
orderBy: {
chunkIndex: 'asc',
},
select: {
events: true,
chunkIndex: true,
eventCount: true,
startedAt: true,
endedAt: true,
},
});
}
@@ -0,0 +1,67 @@
import prisma from '@/lib/prisma';
import type { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionReplays';
export async function getSessionReplays(...args: [websiteId: string, filters: QueryFilters]) {
return relationalQuery(...args);
}
async function relationalQuery(websiteId: string, filters: QueryFilters) {
const { pagedRawQuery, parseFilters } = prisma;
const { search, startDate, endDate } = filters;
const { queryParams } = parseFilters({
...filters,
websiteId,
search: search ? `%${search}%` : undefined,
});
let dateQuery = '';
if (startDate && endDate) {
dateQuery = `and sr.created_at between {{startDate}} and {{endDate}}`;
} else if (startDate) {
dateQuery = `and sr.created_at >= {{startDate}}`;
}
const searchQuery = search
? `and (session.distinct_id ilike {{search}}
or session.city ilike {{search}}
or session.browser ilike {{search}})`
: '';
return pagedRawQuery(
`
select
sr.session_id as "id",
sr.website_id as "websiteId",
session.browser,
session.os,
session.device,
session.country,
session.city,
sum(sr.event_count) as "eventCount",
count(sr.replay_id) as "chunkCount",
min(sr.started_at) as "startedAt",
max(sr.ended_at) as "endedAt",
(extract(epoch from max(sr.ended_at) - min(sr.started_at)) * 1000)::bigint as "duration",
max(sr.created_at) as "createdAt"
from session_replay sr
left join session on session.session_id = sr.session_id
and session.website_id = sr.website_id
where sr.website_id = {{websiteId::uuid}}
${dateQuery}
${searchQuery}
group by sr.session_id,
sr.website_id,
session.browser,
session.os,
session.device,
session.country,
session.city
order by max(sr.created_at) desc
`,
queryParams,
filters,
FUNCTION_NAME,
);
}
+4
View File
@@ -0,0 +1,4 @@
export * from './deleteReplaysByWebsite';
export * from './getReplayChunks';
export * from './getSessionReplays';
export * from './saveReplayChunk';
@@ -0,0 +1,39 @@
import { uuid } from '@/lib/crypto';
import prisma from '@/lib/prisma';
export interface SaveReplayChunkArgs {
websiteId: string;
sessionId: string;
chunkIndex: number;
events: Uint8Array;
eventCount: number;
startedAt: Date;
endedAt: Date;
}
export async function saveReplayChunk(args: SaveReplayChunkArgs) {
return relationalQuery(args);
}
async function relationalQuery({
websiteId,
sessionId,
chunkIndex,
events,
eventCount,
startedAt,
endedAt,
}: SaveReplayChunkArgs) {
return prisma.client.sessionReplay.create({
data: {
id: uuid(),
websiteId,
sessionId,
chunkIndex,
events: new Uint8Array(events) as any,
eventCount,
startedAt,
endedAt,
},
});
}