remove unused replay snapshot path before merge
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user