Heatmaps bug fixes, update schema to match overlay, use react-zen component to match ui/ux, polish

This commit is contained in:
Francis Cao
2026-05-14 14:23:41 -07:00
parent a22274bbbc
commit cd20b3aafd
18 changed files with 1262 additions and 628 deletions
@@ -18,6 +18,13 @@ export interface ExtractedHeatmapEvent {
scrollPct: number | null;
urlPath: string;
createdAt: Date;
replayChunkIndex: number | null;
replayEventIndex: number | null;
replayTimeMs: number | null;
}
interface ExtractHeatmapEventOptions {
chunkIndex?: number;
}
function safePathname(href: unknown): string | null {
@@ -29,7 +36,10 @@ function safePathname(href: unknown): string | null {
}
}
export function extractHeatmapEvents(events: any[]): ExtractedHeatmapEvent[] {
export function extractHeatmapEvents(
events: any[],
{ chunkIndex }: ExtractHeatmapEventOptions = {},
): ExtractedHeatmapEvent[] {
if (!Array.isArray(events) || events.length === 0) return [];
let urlPath: string | null = null;
@@ -37,8 +47,10 @@ export function extractHeatmapEvents(events: any[]): ExtractedHeatmapEvent[] {
let viewportH: number | null = null;
const out: ExtractedHeatmapEvent[] = [];
for (const ev of events) {
for (const [eventIndex, ev] of events.entries()) {
if (!ev || typeof ev !== 'object') continue;
const replayTimeMs =
typeof ev.timestamp === 'number' && Number.isFinite(ev.timestamp) ? ev.timestamp : null;
if (ev.type === RRWEB_TYPE_META && ev.data) {
const path = safePathname(ev.data.href);
@@ -72,7 +84,10 @@ export function extractHeatmapEvents(events: any[]): ExtractedHeatmapEvent[] {
? Math.max(0, Math.min(100, Math.round(p.scrollPct)))
: null,
urlPath: path,
createdAt: new Date(typeof ev.timestamp === 'number' ? ev.timestamp : Date.now()),
createdAt: new Date(replayTimeMs ?? Date.now()),
replayChunkIndex: chunkIndex ?? null,
replayEventIndex: eventIndex,
replayTimeMs,
});
}
continue;
@@ -103,7 +118,10 @@ export function extractHeatmapEvents(events: any[]): ExtractedHeatmapEvent[] {
pageH: null,
scrollPct: null,
urlPath,
createdAt: new Date(typeof ev.timestamp === 'number' ? ev.timestamp : Date.now()),
createdAt: new Date(replayTimeMs ?? Date.now()),
replayChunkIndex: chunkIndex ?? null,
replayEventIndex: eventIndex,
replayTimeMs,
});
}
}
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -18,6 +18,9 @@ export interface HeatmapEventRow {
pageH: number | null;
scrollPct: number | null;
createdAt: Date;
replayChunkIndex: number | null;
replayEventIndex: number | null;
replayTimeMs: number | null;
}
export async function saveHeatmapEvents(rows: HeatmapEventRow[]) {
@@ -46,7 +49,10 @@ async function relationalQuery(rows: HeatmapEventRow[]) {
pageH: r.pageH,
scrollPct: r.scrollPct,
createdAt: r.createdAt,
})),
replayChunkIndex: r.replayChunkIndex,
replayEventIndex: r.replayEventIndex,
replayTimeMs: r.replayTimeMs,
})) as any,
});
}
@@ -69,6 +75,9 @@ async function clickhouseQuery(rows: HeatmapEventRow[]) {
page_h: r.pageH,
scroll_pct: r.scrollPct,
created_at: getUTCString(r.createdAt),
replay_chunk_index: r.replayChunkIndex,
replay_event_index: r.replayEventIndex,
replay_time_ms: r.replayTimeMs,
}));
if (kafka.enabled) {
+51 -8
View File
@@ -15,15 +15,39 @@ export interface ReplayChunk {
endedAt: Date;
}
export async function getReplayChunks(websiteId: string, visitId: string): Promise<ReplayChunk[]> {
interface GetReplayChunksOptions {
endAt?: Date;
endChunkIndex?: number;
}
export async function getReplayChunks(
websiteId: string,
visitId: string,
options: GetReplayChunksOptions = {},
): Promise<ReplayChunk[]> {
return runQuery({
[PRISMA]: () => relationalQuery(websiteId, visitId),
[CLICKHOUSE]: () => clickhouseQuery(websiteId, visitId),
[PRISMA]: () => relationalQuery(websiteId, visitId, options),
[CLICKHOUSE]: () => clickhouseQuery(websiteId, visitId, options),
});
}
async function relationalQuery(websiteId: string, visitId: string): Promise<ReplayChunk[]> {
async function relationalQuery(
websiteId: string,
visitId: string,
{ endAt, endChunkIndex }: GetReplayChunksOptions,
): Promise<ReplayChunk[]> {
const { rawQuery } = prisma;
const endAtFilter = endAt
? `
and started_at <= {{endAt}}
`
: '';
const endChunkFilter =
endChunkIndex !== undefined
? `
and chunk_index <= {{endChunkIndex}}
`
: '';
const chunks: {
sessionId: string;
@@ -46,9 +70,11 @@ async function relationalQuery(websiteId: string, visitId: string): Promise<Repl
from session_replay
where website_id = {{websiteId::uuid}}
and visit_id = {{visitId::uuid}}
${endAtFilter}
${endChunkFilter}
order by chunk_index asc
`,
{ websiteId, visitId },
{ websiteId, visitId, endAt, endChunkIndex },
FUNCTION_NAME,
);
@@ -58,8 +84,23 @@ async function relationalQuery(websiteId: string, visitId: string): Promise<Repl
}));
}
async function clickhouseQuery(websiteId: string, visitId: string): Promise<ReplayChunk[]> {
async function clickhouseQuery(
websiteId: string,
visitId: string,
{ endAt, endChunkIndex }: GetReplayChunksOptions,
): Promise<ReplayChunk[]> {
const { rawQuery } = clickhouse;
const endAtFilter = endAt
? `
and started_at <= {endAt:DateTime64}
`
: '';
const endChunkFilter =
endChunkIndex !== undefined
? `
and chunk_index <= {endChunkIndex:UInt32}
`
: '';
const results = await rawQuery<
{
@@ -82,11 +123,13 @@ async function clickhouseQuery(websiteId: string, visitId: string): Promise<Repl
started_at,
ended_at
from session_replay
where website_id = {websiteId:UUID}
prewhere website_id = {websiteId:UUID}
and visit_id = {visitId:UUID}
${endAtFilter}
${endChunkFilter}
order by chunk_index asc
`,
{ websiteId, visitId },
{ websiteId, visitId, endAt, endChunkIndex },
FUNCTION_NAME,
);