remove unused replay snapshot path before merge

This commit is contained in:
Francis Cao
2026-05-22 11:09:57 -07:00
parent 462be82782
commit b5d56a2a29
3 changed files with 31 additions and 177 deletions
@@ -3,11 +3,6 @@ import { json, unauthorized } from '@/lib/response';
import { canViewWebsite } from '@/permissions'; import { canViewWebsite } from '@/permissions';
import { getReplayChunks } from '@/queries/sql'; import { getReplayChunks } from '@/queries/sql';
const RRWEB_TYPE_FULL_SNAPSHOT = 2;
const RRWEB_TYPE_META = 4;
const SNAPSHOT_WINDOW_CHUNK_LIMIT = 6;
const SNAPSHOT_WINDOW_MAX_CHUNKS = 96;
function getEventTimestamp(event: any): number | null { function getEventTimestamp(event: any): number | null {
const timestamp = Number(event?.timestamp); const timestamp = Number(event?.timestamp);
@@ -24,102 +19,6 @@ function parseOptionalInteger(value: string | null): number | undefined {
return Number.isInteger(parsed) ? parsed : undefined; return Number.isInteger(parsed) ? parsed : undefined;
} }
function trimChunksToCheckpoint(
chunks: Awaited<ReturnType<typeof getReplayChunks>>,
{ endChunkIndex, endEventIndex }: { endChunkIndex?: number; endEventIndex?: number },
) {
let lastMetaChunkIndex: number | null = null;
let checkpointStartChunkIndex: number | null = null;
let hasMeta = false;
for (const chunk of chunks) {
if (endChunkIndex !== undefined && chunk.chunkIndex > endChunkIndex) {
break;
}
for (let chunkEventIndex = 0; chunkEventIndex < chunk.events.length; chunkEventIndex++) {
if (
chunk.chunkIndex === endChunkIndex &&
endEventIndex !== undefined &&
chunkEventIndex > endEventIndex
) {
break;
}
const event = chunk.events[chunkEventIndex];
if (event?.type === RRWEB_TYPE_META) {
lastMetaChunkIndex = chunk.chunkIndex;
hasMeta = true;
}
if (event?.type === RRWEB_TYPE_FULL_SNAPSHOT) {
checkpointStartChunkIndex = lastMetaChunkIndex ?? chunk.chunkIndex;
}
}
}
if (checkpointStartChunkIndex === null) {
return { chunks, foundCheckpoint: false, hasMeta };
}
return {
chunks: chunks.filter(chunk => chunk.chunkIndex >= checkpointStartChunkIndex),
foundCheckpoint: true,
hasMeta,
};
}
async function getReplaySnapshotChunks(
websiteId: string,
replayId: string,
{ endAt, endChunkIndex, endEventIndex }: { endAt?: Date; endChunkIndex: number; endEventIndex?: number },
) {
let limit = SNAPSHOT_WINDOW_CHUNK_LIMIT;
while (limit <= SNAPSHOT_WINDOW_MAX_CHUNKS) {
const descChunks = await getReplayChunks(websiteId, replayId, {
endAt,
endChunkIndex,
limit,
order: 'desc',
});
const chunks = [...descChunks].reverse();
const { chunks: trimmedChunks, foundCheckpoint, hasMeta } = trimChunksToCheckpoint(chunks, {
endChunkIndex,
endEventIndex,
});
if (foundCheckpoint || descChunks.length < limit) {
if (hasMeta || trimmedChunks.length === 0) {
return trimmedChunks;
}
const initialChunks = await getReplayChunks(websiteId, replayId, {
limit: 1,
order: 'asc',
});
if (!initialChunks.length) {
return trimmedChunks;
}
const initialChunk = initialChunks[0];
if (trimmedChunks.some(chunk => chunk.chunkIndex === initialChunk.chunkIndex)) {
return trimmedChunks;
}
return [initialChunk, ...trimmedChunks];
}
limit *= 2;
}
return getReplayChunks(websiteId, replayId, { endAt, endChunkIndex });
}
function mergeReplayEvents( function mergeReplayEvents(
chunks: Awaited<ReturnType<typeof getReplayChunks>>, chunks: Awaited<ReturnType<typeof getReplayChunks>>,
{ {
@@ -191,14 +90,7 @@ export async function GET(
return unauthorized(); return unauthorized();
} }
const chunks = const chunks = await getReplayChunks(websiteId, replayId, { endAt, endChunkIndex });
endChunkIndex !== undefined
? await getReplaySnapshotChunks(websiteId, replayId, {
endAt,
endChunkIndex,
endEventIndex,
})
: await getReplayChunks(websiteId, replayId, { endAt, endChunkIndex });
const allEvents = mergeReplayEvents(chunks, { until, endChunkIndex, endEventIndex }); const allEvents = mergeReplayEvents(chunks, { until, endChunkIndex, endEventIndex });
const sessionId = chunks.length > 0 ? chunks[0].sessionId : null; const sessionId = chunks.length > 0 ? chunks[0].sessionId : null;
const startedAt = chunks.length > 0 ? chunks[0].startedAt : null; const startedAt = chunks.length > 0 ? chunks[0].startedAt : null;
+24 -31
View File
@@ -3,16 +3,15 @@ import { HEATMAP_EVENT_TYPE } from '@/lib/constants';
const RRWEB_TYPE_INCREMENTAL = 3; const RRWEB_TYPE_INCREMENTAL = 3;
const RRWEB_TYPE_META = 4; const RRWEB_TYPE_META = 4;
const RRWEB_TYPE_CUSTOM = 5; const RRWEB_TYPE_CUSTOM = 5;
const RRWEB_SOURCE_MOUSE_INTERACTION = 2;
const RRWEB_SOURCE_VIEWPORT_RESIZE = 4; const RRWEB_SOURCE_VIEWPORT_RESIZE = 4;
const RRWEB_MOUSE_CLICK = 2;
export interface ExtractedHeatmapEvent { export interface ExtractedHeatmapEvent {
eventType: number; eventType: number;
nodeId: number | null; nodeId: number | null;
x: number | null; x: number | null;
y: number | null; y: number | null;
pageX: number | null;
pageY: number | null;
pageW: number | null;
viewportW: number | null; viewportW: number | null;
viewportH: number | null; viewportH: number | null;
pageH: number | null; pageH: number | null;
@@ -77,9 +76,6 @@ export function extractHeatmapEvents(
nodeId: null, nodeId: null,
x: null, x: null,
y: null, y: null,
pageX: null,
pageY: null,
pageW: typeof p.pageW === 'number' ? p.pageW : null,
viewportW: typeof p.viewportW === 'number' ? p.viewportW : viewportW, viewportW: typeof p.viewportW === 'number' ? p.viewportW : viewportW,
viewportH: typeof p.viewportH === 'number' ? p.viewportH : viewportH, viewportH: typeof p.viewportH === 'number' ? p.viewportH : viewportH,
pageH: typeof p.pageH === 'number' ? p.pageH : null, pageH: typeof p.pageH === 'number' ? p.pageH : null,
@@ -94,31 +90,6 @@ export function extractHeatmapEvents(
replayTimeMs, replayTimeMs,
}); });
} }
if (ev.data.tag === 'heatmap-click' && ev.data.payload) {
const p = ev.data.payload;
const path = safePathname(p.url) ?? urlPath;
if (path === null) continue;
out.push({
eventType: HEATMAP_EVENT_TYPE.click,
nodeId: null,
x: typeof p.x === 'number' ? Math.round(p.x) : null,
y: typeof p.y === 'number' ? Math.round(p.y) : null,
pageX: typeof p.pageX === 'number' ? Math.round(p.pageX) : null,
pageY: typeof p.pageY === 'number' ? Math.round(p.pageY) : null,
pageW: typeof p.pageW === 'number' ? Math.round(p.pageW) : null,
viewportW: typeof p.viewportW === 'number' ? Math.round(p.viewportW) : viewportW,
viewportH: typeof p.viewportH === 'number' ? Math.round(p.viewportH) : viewportH,
pageH: typeof p.pageH === 'number' ? Math.round(p.pageH) : null,
scrollPct: null,
urlPath: path,
createdAt: new Date(replayTimeMs ?? Date.now()),
replayChunkIndex: chunkIndex ?? null,
replayEventIndex: eventIndex,
replayTimeMs,
});
}
continue; continue;
} }
@@ -131,6 +102,28 @@ export function extractHeatmapEvents(
if (typeof d.height === 'number') viewportH = d.height; if (typeof d.height === 'number') viewportH = d.height;
continue; continue;
} }
if (
d.source === RRWEB_SOURCE_MOUSE_INTERACTION &&
d.type === RRWEB_MOUSE_CLICK &&
urlPath !== null
) {
out.push({
eventType: HEATMAP_EVENT_TYPE.click,
nodeId: typeof d.id === 'number' ? d.id : null,
x: typeof d.x === 'number' ? Math.round(d.x) : null,
y: typeof d.y === 'number' ? Math.round(d.y) : null,
viewportW,
viewportH,
pageH: null,
scrollPct: null,
urlPath,
createdAt: new Date(replayTimeMs ?? Date.now()),
replayChunkIndex: chunkIndex ?? null,
replayEventIndex: eventIndex,
replayTimeMs,
});
}
} }
return out; return out;
+6 -37
View File
@@ -18,9 +18,6 @@ export interface ReplayChunk {
interface GetReplayChunksOptions { interface GetReplayChunksOptions {
endAt?: Date; endAt?: Date;
endChunkIndex?: number; endChunkIndex?: number;
startChunkIndex?: number;
limit?: number;
order?: 'asc' | 'desc';
} }
export async function getReplayChunks( export async function getReplayChunks(
@@ -37,7 +34,7 @@ export async function getReplayChunks(
async function relationalQuery( async function relationalQuery(
websiteId: string, websiteId: string,
visitId: string, visitId: string,
{ endAt, endChunkIndex, startChunkIndex, limit, order = 'asc' }: GetReplayChunksOptions, { endAt, endChunkIndex }: GetReplayChunksOptions,
): Promise<ReplayChunk[]> { ): Promise<ReplayChunk[]> {
const { rawQuery } = prisma; const { rawQuery } = prisma;
const endAtFilter = endAt const endAtFilter = endAt
@@ -51,18 +48,6 @@ async function relationalQuery(
and chunk_index <= {{endChunkIndex}} and chunk_index <= {{endChunkIndex}}
` `
: ''; : '';
const startChunkFilter =
startChunkIndex !== undefined
? `
and chunk_index >= {{startChunkIndex}}
`
: '';
const limitClause =
limit !== undefined
? `
limit ${limit}
`
: '';
const chunks: { const chunks: {
sessionId: string; sessionId: string;
@@ -86,12 +71,10 @@ async function relationalQuery(
where website_id = {{websiteId::uuid}} where website_id = {{websiteId::uuid}}
and visit_id = {{visitId::uuid}} and visit_id = {{visitId::uuid}}
${endAtFilter} ${endAtFilter}
${startChunkFilter}
${endChunkFilter} ${endChunkFilter}
order by chunk_index ${order} order by chunk_index asc
${limitClause}
`, `,
{ websiteId, visitId, endAt, endChunkIndex, startChunkIndex }, { websiteId, visitId, endAt, endChunkIndex },
FUNCTION_NAME, FUNCTION_NAME,
); );
@@ -104,7 +87,7 @@ async function relationalQuery(
async function clickhouseQuery( async function clickhouseQuery(
websiteId: string, websiteId: string,
visitId: string, visitId: string,
{ endAt, endChunkIndex, startChunkIndex, limit, order = 'asc' }: GetReplayChunksOptions, { endAt, endChunkIndex }: GetReplayChunksOptions,
): Promise<ReplayChunk[]> { ): Promise<ReplayChunk[]> {
const { rawQuery } = clickhouse; const { rawQuery } = clickhouse;
const endAtFilter = endAt const endAtFilter = endAt
@@ -118,18 +101,6 @@ async function clickhouseQuery(
and chunk_index <= {endChunkIndex:UInt32} and chunk_index <= {endChunkIndex:UInt32}
` `
: ''; : '';
const startChunkFilter =
startChunkIndex !== undefined
? `
and chunk_index >= {startChunkIndex:UInt32}
`
: '';
const limitClause =
limit !== undefined
? `
limit ${limit}
`
: '';
const results = await rawQuery< const results = await rawQuery<
{ {
@@ -155,12 +126,10 @@ async function clickhouseQuery(
prewhere website_id = {websiteId:UUID} prewhere website_id = {websiteId:UUID}
and visit_id = {visitId:UUID} and visit_id = {visitId:UUID}
${endAtFilter} ${endAtFilter}
${startChunkFilter}
${endChunkFilter} ${endChunkFilter}
order by chunk_index ${order} order by chunk_index asc
${limitClause}
`, `,
{ websiteId, visitId, endAt, endChunkIndex, startChunkIndex }, { websiteId, visitId, endAt, endChunkIndex },
FUNCTION_NAME, FUNCTION_NAME,
); );