diff --git a/db/clickhouse/migrations/12_add_heatmap.sql b/db/clickhouse/migrations/12_add_heatmap.sql index db44666b3..f6bb316c4 100644 --- a/db/clickhouse/migrations/12_add_heatmap.sql +++ b/db/clickhouse/migrations/12_add_heatmap.sql @@ -7,7 +7,6 @@ CREATE TABLE umami.heatmap_event visit_id UUID, url_path String, event_type UInt8, - node_id Nullable(Int32), x Nullable(Int32), y Nullable(Int32), page_x Nullable(Int32), @@ -17,34 +16,9 @@ CREATE TABLE umami.heatmap_event viewport_h Nullable(Int32), page_h Nullable(Int32), scroll_pct Nullable(UInt8), - replay_chunk_index Nullable(UInt32), - replay_event_index Nullable(UInt32), - replay_time_ms Nullable(Int64), created_at DateTime('UTC') ) ENGINE = MergeTree PARTITION BY toYYYYMM(created_at) ORDER BY (website_id, url_path, event_type, created_at) - SETTINGS index_granularity = 8192; - --- Create heatmap_snapshot -CREATE TABLE umami.heatmap_snapshot -( - snapshot_id UUID, - website_id UUID, - url_path String, - viewport_w UInt32, - viewport_h UInt32, - page_w UInt32, - page_h UInt32, - status UInt8, - mime_type LowCardinality(String), - object_key String, - image_size Nullable(UInt32), - error Nullable(String), - created_at DateTime('UTC') -) -ENGINE = MergeTree - PARTITION BY toYYYYMM(created_at) - ORDER BY (website_id, url_path, viewport_w, viewport_h, created_at) - SETTINGS index_granularity = 8192; + SETTINGS index_granularity = 8192; \ No newline at end of file diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index 75943488c..abcbe1434 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -409,7 +409,6 @@ CREATE TABLE umami.heatmap_event visit_id UUID, url_path String, event_type UInt8, - node_id Nullable(Int32), x Nullable(Int32), y Nullable(Int32), page_x Nullable(Int32), @@ -419,34 +418,9 @@ CREATE TABLE umami.heatmap_event viewport_h Nullable(Int32), page_h Nullable(Int32), scroll_pct Nullable(UInt8), - replay_chunk_index Nullable(UInt32), - replay_event_index Nullable(UInt32), - replay_time_ms Nullable(Int64), created_at DateTime('UTC') ) ENGINE = MergeTree PARTITION BY toYYYYMM(created_at) ORDER BY (website_id, url_path, event_type, created_at) SETTINGS index_granularity = 8192; - --- Create heatmap_snapshot -CREATE TABLE umami.heatmap_snapshot -( - snapshot_id UUID, - website_id UUID, - url_path String, - viewport_w UInt32, - viewport_h UInt32, - page_w UInt32, - page_h UInt32, - status UInt8, - mime_type LowCardinality(String), - object_key String, - image_size Nullable(UInt32), - error Nullable(String), - created_at DateTime('UTC') -) -ENGINE = MergeTree - PARTITION BY toYYYYMM(created_at) - ORDER BY (website_id, url_path, viewport_w, viewport_h, created_at) - SETTINGS index_granularity = 8192; diff --git a/prisma/migrations/20_add_heatmap/migration.sql b/prisma/migrations/20_add_heatmap/migration.sql index f3e3d30f5..4263c921c 100644 --- a/prisma/migrations/20_add_heatmap/migration.sql +++ b/prisma/migrations/20_add_heatmap/migration.sql @@ -12,7 +12,6 @@ CREATE TABLE "heatmap_event" ( "visit_id" UUID NOT NULL, "url_path" VARCHAR(500) NOT NULL, "event_type" INTEGER NOT NULL, - "node_id" INTEGER, "x" INTEGER, "y" INTEGER, "page_x" INTEGER, @@ -22,9 +21,6 @@ CREATE TABLE "heatmap_event" ( "viewport_h" INTEGER, "page_h" INTEGER, "scroll_pct" INTEGER, - "replay_chunk_index" INTEGER, - "replay_event_index" INTEGER, - "replay_time_ms" BIGINT, "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "heatmap_event_pkey" PRIMARY KEY ("heatmap_event_id") @@ -34,32 +30,4 @@ CREATE TABLE "heatmap_event" ( CREATE INDEX "heatmap_event_website_id_idx" ON "heatmap_event"("website_id"); CREATE INDEX "heatmap_event_visit_id_idx" ON "heatmap_event"("visit_id"); CREATE INDEX "heatmap_event_website_id_created_at_idx" ON "heatmap_event"("website_id", "created_at"); -CREATE INDEX "heatmap_event_website_id_url_path_event_type_created_at_idx" ON "heatmap_event"("website_id", "url_path", "event_type", "created_at"); -CREATE INDEX "heatmap_event_website_id_visit_id_replay_chunk_index_replay_event_index_idx" ON "heatmap_event"("website_id", "visit_id", "replay_chunk_index", "replay_event_index"); - --- CreateTable -CREATE TABLE "heatmap_snapshot" ( - "snapshot_id" UUID NOT NULL, - "website_id" UUID NOT NULL, - "url_path" VARCHAR(500) NOT NULL, - "viewport_w" INTEGER NOT NULL, - "viewport_h" INTEGER NOT NULL, - "page_w" INTEGER NOT NULL, - "page_h" INTEGER NOT NULL, - "status" VARCHAR(20) NOT NULL, - "mime_type" VARCHAR(100), - "image_data" BYTEA, - "image_size" INTEGER, - "error" VARCHAR(500), - "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ(6), - - CONSTRAINT "heatmap_snapshot_pkey" PRIMARY KEY ("snapshot_id") -); - --- CreateIndex -CREATE UNIQUE INDEX "heatmap_snapshot_website_id_url_path_viewport_w_viewport_h_key" -ON "heatmap_snapshot"("website_id", "url_path", "viewport_w", "viewport_h"); -CREATE INDEX "heatmap_snapshot_website_id_idx" ON "heatmap_snapshot"("website_id"); -CREATE INDEX "heatmap_snapshot_website_id_url_path_idx" ON "heatmap_snapshot"("website_id", "url_path"); -CREATE INDEX "heatmap_snapshot_website_id_updated_at_idx" ON "heatmap_snapshot"("website_id", "updated_at"); +CREATE INDEX "heatmap_event_website_id_url_path_event_type_created_at_idx" ON "heatmap_event"("website_id", "url_path", "event_type", "created_at"); \ No newline at end of file diff --git a/prisma/migrations/21_add_heatmap_preview/migration.sql b/prisma/migrations/21_add_heatmap_preview/migration.sql deleted file mode 100644 index 6b7d162a3..000000000 --- a/prisma/migrations/21_add_heatmap_preview/migration.sql +++ /dev/null @@ -1,38 +0,0 @@ --- AlterTable -ALTER TABLE "heatmap_snapshot" ALTER COLUMN "created_at" DROP NOT NULL; - --- CreateTable -CREATE TABLE "heatmap_replay_preview" ( - "preview_id" UUID NOT NULL, - "website_id" UUID NOT NULL, - "session_id" UUID NOT NULL, - "visit_id" UUID NOT NULL, - "url_path" VARCHAR(500) NOT NULL, - "viewport_w" INTEGER NOT NULL, - "viewport_h" INTEGER NOT NULL, - "replay_chunk_index" INTEGER NOT NULL, - "replay_event_index" INTEGER NOT NULL, - "replay_time_ms" BIGINT, - "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ(6), - - CONSTRAINT "heatmap_replay_preview_pkey" PRIMARY KEY ("preview_id") -); - --- CreateIndex -CREATE INDEX "heatmap_replay_preview_website_id_idx" ON "heatmap_replay_preview"("website_id"); - --- CreateIndex -CREATE INDEX "heatmap_replay_preview_visit_id_idx" ON "heatmap_replay_preview"("visit_id"); - --- CreateIndex -CREATE INDEX "heatmap_replay_preview_website_id_url_path_idx" ON "heatmap_replay_preview"("website_id", "url_path"); - --- CreateIndex -CREATE UNIQUE INDEX "heatmap_replay_preview_website_id_url_path_viewport_w_viewp_key" ON "heatmap_replay_preview"("website_id", "url_path", "viewport_w", "viewport_h"); - --- CreateIndex -CREATE INDEX "session_replay_visit_id_idx" ON "session_replay"("visit_id"); - --- RenameIndex -ALTER INDEX "heatmap_event_website_id_visit_id_replay_chunk_index_replay_eve" RENAME TO "heatmap_event_website_id_visit_id_replay_chunk_index_replay_idx"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1ba608574..1c285de47 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -89,8 +89,6 @@ model Website { sessionReplays SessionReplay[] sessionReplaysSaved SessionReplaySaved[] heatmapEvents HeatmapEvent[] - heatmapReplayPreviews HeatmapReplayPreview[] - heatmapSnapshots HeatmapSnapshot[] @@index([userId]) @@index([teamId]) @@ -410,7 +408,6 @@ model HeatmapEvent { visitId String @map("visit_id") @db.Uuid urlPath String @map("url_path") @db.VarChar(500) eventType Int @map("event_type") @db.Integer - nodeId Int? @map("node_id") @db.Integer x Int? @db.Integer y Int? @db.Integer pageX Int? @map("page_x") @db.Integer @@ -420,9 +417,6 @@ model HeatmapEvent { viewportH Int? @map("viewport_h") @db.Integer pageH Int? @map("page_h") @db.Integer scrollPct Int? @map("scroll_pct") @db.Integer - replayChunkIndex Int? @map("replay_chunk_index") @db.Integer - replayEventIndex Int? @map("replay_event_index") @db.Integer - replayTimeMs BigInt? @map("replay_time_ms") @db.BigInt createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) website Website @relation(fields: [websiteId], references: [id]) @@ -431,54 +425,5 @@ model HeatmapEvent { @@index([visitId]) @@index([websiteId, createdAt]) @@index([websiteId, urlPath, eventType, createdAt]) - @@index([websiteId, visitId, replayChunkIndex, replayEventIndex]) @@map("heatmap_event") } - -model HeatmapReplayPreview { - id String @id() @map("preview_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - sessionId String @map("session_id") @db.Uuid - visitId String @map("visit_id") @db.Uuid - urlPath String @map("url_path") @db.VarChar(500) - viewportW Int @map("viewport_w") @db.Integer - viewportH Int @map("viewport_h") @db.Integer - replayChunkIndex Int @map("replay_chunk_index") @db.Integer - replayEventIndex Int @map("replay_event_index") @db.Integer - replayTimeMs BigInt? @map("replay_time_ms") @db.BigInt - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - - website Website @relation(fields: [websiteId], references: [id]) - - @@unique([websiteId, urlPath, viewportW, viewportH]) - @@index([websiteId]) - @@index([visitId]) - @@index([websiteId, urlPath]) - @@map("heatmap_replay_preview") -} - -model HeatmapSnapshot { - id String @id() @map("snapshot_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - urlPath String @map("url_path") @db.VarChar(500) - viewportW Int @map("viewport_w") @db.Integer - viewportH Int @map("viewport_h") @db.Integer - pageW Int @map("page_w") @db.Integer - pageH Int @map("page_h") @db.Integer - status String @db.VarChar(20) - mimeType String? @map("mime_type") @db.VarChar(100) - imageData Bytes? @map("image_data") - imageSize Int? @map("image_size") @db.Integer - error String? @db.VarChar(500) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - - website Website @relation(fields: [websiteId], references: [id]) - - @@unique([websiteId, urlPath, viewportW, viewportH]) - @@index([websiteId]) - @@index([websiteId, urlPath]) - @@index([websiteId, updatedAt]) - @@map("heatmap_snapshot") -} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx index 7eef4d20f..9dec06fa7 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx @@ -14,7 +14,6 @@ import { Laptop, Monitor, Smartphone, Tablet } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useResultQuery } from '@/components/hooks'; -import { getClientAuthToken } from '@/lib/client'; import { formatLongNumber } from '@/lib/format'; import type { HeatmapMode, HeatmapPoint, HeatmapResult, HeatmapSnapshot } from '@/queries/sql'; import styles from './Heatmap.module.css'; @@ -882,90 +881,14 @@ function SnapshotPreview({ snapshot: HeatmapSnapshot; onReady: () => void; }) { - if (snapshot.kind === 'iframe') { - return ; - } - - return ; -} - -function SnapshotImage({ - snapshot, - onReady, -}: { - snapshot: Extract; - onReady: () => void; -}) { - const [src, setSrc] = useState(null); - const imageUrl = snapshot.imageUrl; - - useEffect(() => { - if (!imageUrl) { - setSrc(null); - onReady(); - return; - } - - const controller = new AbortController(); - const token = getClientAuthToken(); - let objectUrl: string | null = null; - - setSrc(null); - - fetch(imageUrl, { - signal: controller.signal, - headers: { - ...(token ? { authorization: `Bearer ${token}` } : {}), - }, - }) - .then(async response => { - if (!response.ok) { - throw new Error(`Snapshot image request failed: ${response.status}`); - } - - const blob = await response.blob(); - objectUrl = URL.createObjectURL(blob); - setSrc(objectUrl); - }) - .catch(() => { - setSrc(null); - onReady(); - }); - - return () => { - controller.abort(); - - if (objectUrl) { - URL.revokeObjectURL(objectUrl); - } - }; - }, [imageUrl, onReady, snapshot.id]); - - const handleLoad = useCallback(() => onReady(), [onReady]); - const imageWidth = Math.max(snapshot.pageW, snapshot.viewportW); - - return ( -
- -
- ); + return ; } function IframeSnapshot({ snapshot, onReady, }: { - snapshot: Extract; + snapshot: HeatmapSnapshot; onReady: () => void; }) { const [available, setAvailable] = useState(true); diff --git a/src/app/api/record/route.ts b/src/app/api/record/route.ts index 479bba1c8..8e305abde 100644 --- a/src/app/api/record/route.ts +++ b/src/app/api/record/route.ts @@ -217,7 +217,6 @@ export async function POST(request: Request) { sessionId, visitId, eventType: event.type === 'click' ? HEATMAP_EVENT_TYPE.click : HEATMAP_EVENT_TYPE.scroll, - nodeId: null, x: event.type === 'click' ? (event.x ?? null) : null, y: event.type === 'click' ? (event.y ?? null) : null, pageX: event.type === 'click' ? (event.pageX ?? null) : null, @@ -229,9 +228,6 @@ export async function POST(request: Request) { scrollPct: event.type === 'scroll' ? (event.scrollPct ?? null) : null, urlPath: getUrlPath(event.url), createdAt: new Date(event.timestamp ?? fallbackMs), - replayChunkIndex: null, - replayEventIndex: null, - replayTimeMs: null, })); if (heatmapRows.length) { diff --git a/src/app/api/websites/[websiteId]/heatmaps/snapshots/[snapshotId]/route.ts b/src/app/api/websites/[websiteId]/heatmaps/snapshots/[snapshotId]/route.ts deleted file mode 100644 index d8f7eaa3e..000000000 --- a/src/app/api/websites/[websiteId]/heatmaps/snapshots/[snapshotId]/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { parseRequest } from '@/lib/request'; -import { notFound, unauthorized } from '@/lib/response'; -import { canViewAuthenticatedWebsite } from '@/permissions'; -import { getHeatmapSnapshotImage } from '@/queries/sql/heatmap/ensureHeatmapSnapshot'; - -export async function GET( - request: Request, - { params }: { params: Promise<{ websiteId: string; snapshotId: string }> }, -) { - const { auth, error } = await parseRequest(request); - const { websiteId, snapshotId } = await params; - - if (error) { - return error(); - } - - if (!(await canViewAuthenticatedWebsite(auth, websiteId))) { - return unauthorized(); - } - - const snapshot = await getHeatmapSnapshotImage(websiteId, snapshotId); - - if (!snapshot) { - return notFound({ message: 'Snapshot not found.' }); - } - - return new Response(new Uint8Array(snapshot.imageData), { - status: 200, - headers: { - 'Content-Type': snapshot.mimeType, - 'Cache-Control': 'private, max-age=300', - }, - }); -} diff --git a/src/lib/heatmap-r2.ts b/src/lib/heatmap-r2.ts deleted file mode 100644 index 4da5d06d6..000000000 --- a/src/lib/heatmap-r2.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; - -let client: S3Client | null = null; - -function getBucket() { - const bucket = process.env.R2_BUCKET; - - if (!bucket) { - throw new Error('R2_BUCKET is not set.'); - } - - return bucket; -} - -function getAccountId() { - const accountId = process.env.R2_ACCOUNT_ID; - - if (!accountId) { - throw new Error('R2_ACCOUNT_ID is not set.'); - } - - return accountId; -} - -function getCredentials() { - const accessKeyId = process.env.R2_ACCESS_KEY_ID; - const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY; - - if (!accessKeyId) { - throw new Error('R2_ACCESS_KEY_ID is not set.'); - } - - if (!secretAccessKey) { - throw new Error('R2_SECRET_ACCESS_KEY is not set.'); - } - - return { - accessKeyId, - secretAccessKey, - }; -} - -function getClient() { - if (!client) { - client = new S3Client({ - region: 'auto', - endpoint: `https://${getAccountId()}.r2.cloudflarestorage.com`, - credentials: getCredentials(), - }); - } - - return client; -} - -export async function putHeatmapSnapshot(objectKey: string, imageData: Buffer, mimeType: string) { - await getClient().send( - new PutObjectCommand({ - Bucket: getBucket(), - Key: objectKey, - Body: imageData, - ContentType: mimeType, - }), - ); -} - -export async function getHeatmapSnapshot(objectKey: string) { - let response; - - try { - response = await getClient().send( - new GetObjectCommand({ - Bucket: getBucket(), - Key: objectKey, - }), - ); - } catch (error: any) { - if ( - error?.name === 'NoSuchKey' || - error?.Code === 'NoSuchKey' || - error?.$metadata?.httpStatusCode === 404 - ) { - return null; - } - - throw error; - } - - if (!response.Body) { - return null; - } - - return { - mimeType: response.ContentType || 'application/octet-stream', - imageData: Buffer.from(await response.Body.transformToByteArray()), - }; -} diff --git a/src/queries/sql/heatmap/ensureHeatmapSnapshot.ts b/src/queries/sql/heatmap/ensureHeatmapSnapshot.ts deleted file mode 100644 index f296cf1b1..000000000 --- a/src/queries/sql/heatmap/ensureHeatmapSnapshot.ts +++ /dev/null @@ -1,877 +0,0 @@ -import { serializeError } from 'serialize-error'; -import { getApiUrl } from '@/lib/api-url'; -import clickhouse from '@/lib/clickhouse'; -import { uuid } from '@/lib/crypto'; -import { getHeatmapSnapshot, putHeatmapSnapshot } from '@/lib/heatmap-r2'; -import prisma from '@/lib/prisma'; -import { getWebsite } from '@/queries/prisma'; - -const SNAPSHOT_STATUS = { - pending: 'pending', - ready: 'ready', - failed: 'failed', -} as const; - -const SNAPSHOT_RETRY_DELAY_MS = 15 * 60 * 1000; -const SNAPSHOT_PENDING_WINDOW_MS = 30 * 1000; -const SNAPSHOT_DEVICE_SCALE_FACTOR = 1; -const SNAPSHOT_UNAVAILABLE_ERROR = 'Page screenshot unavailable.'; -const SNAPSHOT_ERROR_MAX_LENGTH = 500; -export type HeatmapSnapshotStatus = (typeof SNAPSHOT_STATUS)[keyof typeof SNAPSHOT_STATUS]; - -export interface HeatmapSnapshotImage { - kind: 'image'; - id: string; - imageUrl: string | null; - status: HeatmapSnapshotStatus; - mimeType: string | null; - pageW: number; - pageH: number; - viewportW: number; - viewportH: number; - error: string | null; -} - -interface SnapshotRecord { - id: string; - websiteId: string; - urlPath: string; - viewportW: number; - viewportH: number; - pageW: number; - pageH: number; - status: HeatmapSnapshotStatus; - mimeType: string | null; - objectKey: string | null; - imageSize: number | null; - error: string | null; - hasImage: boolean; - updatedAt: Date | string | null; -} - -interface EnsureHeatmapSnapshotOptions { - websiteId: string; - urlPath: string; - viewportW: number | null; - viewportH: number | null; - pageW: number | null; - pageH: number | null; -} - -interface CaptureResult { - imageData: Buffer; - mimeType: string; - pageW: number; - pageH: number; -} - -const CLICKHOUSE_SNAPSHOT_STATUS = { - pending: 0, - ready: 1, - failed: 2, -} as const; - -async function measurePage(page: any) { - return page.evaluate(() => { - const doc = document.documentElement; - const body = document.body; - const root = document.scrollingElement || doc || body; - const rootClientWidth = root?.clientWidth || 0; - const docClientWidth = doc?.clientWidth || 0; - const bodyClientWidth = body?.clientWidth || 0; - const visibleWidth = Math.max( - window.innerWidth, - rootClientWidth, - docClientWidth, - bodyClientWidth, - ); - const rootScrollWidth = root?.scrollWidth || 0; - const docScrollWidth = doc?.scrollWidth || 0; - const bodyScrollWidth = body?.scrollWidth || 0; - const horizontalOverflow = Math.max( - rootScrollWidth - rootClientWidth, - docScrollWidth - docClientWidth, - bodyScrollWidth - bodyClientWidth, - 0, - ); - let maxRight = 0; - let maxBottom = 0; - - if (body) { - const walker = document.createTreeWalker(body, NodeFilter.SHOW_ELEMENT); - let node = walker.currentNode as Element | null; - - while (node) { - const rect = node.getBoundingClientRect?.(); - - if (rect && (rect.width > 0 || rect.height > 0)) { - maxRight = Math.max(maxRight, rect.right); - maxBottom = Math.max(maxBottom, rect.bottom); - } - - node = walker.nextNode() as Element | null; - } - } - - const pageW = - horizontalOverflow > 24 - ? Math.max(visibleWidth, rootScrollWidth, docScrollWidth, bodyScrollWidth) - : visibleWidth; - const pageH = Math.max( - window.innerHeight, - root?.scrollHeight || 0, - doc?.scrollHeight || 0, - body?.scrollHeight || 0, - Math.ceil(maxBottom), - ); - - return { - pageW: Math.ceil(pageW), - pageH: Math.ceil(pageH), - }; - }); -} - -async function warmLazyContent(page: any) { - await page.evaluate(async () => { - const root = document.scrollingElement || document.documentElement; - - if (!root) { - return; - } - - const maxScrollTop = Math.max(0, root.scrollHeight - window.innerHeight); - - if (maxScrollTop <= 0) { - return; - } - - const targets = [ - Math.min(window.innerHeight, maxScrollTop), - Math.min(Math.round(maxScrollTop * 0.25), maxScrollTop), - Math.min(Math.round(maxScrollTop * 0.5), maxScrollTop), - Math.min(Math.round(maxScrollTop * 0.75), maxScrollTop), - maxScrollTop, - ].filter((value, index, values) => value > 0 && values.indexOf(value) === index); - - for (const top of targets) { - window.scrollTo(0, top); - await new Promise(resolve => window.setTimeout(resolve, 350)); - } - - window.scrollTo(0, 0); - await new Promise(resolve => window.setTimeout(resolve, 250)); - }); -} - -function getSchema() { - const databaseUrl = process.env.DATABASE_URL; - - if (!databaseUrl) { - return null; - } - - try { - const connectionUrl = new URL(databaseUrl); - - return connectionUrl.searchParams.get('schema'); - } catch { - return null; - } -} - -async function rawExecute(sql: string, data: Record = {}) { - const params: any[] = []; - const schema = getSchema(); - - if (schema) { - await prisma.client.$executeRawUnsafe(`SET search_path TO "${schema}";`); - } - - const query = sql.replaceAll(/\{\{\s*(\w+)(::\w+)?\s*}}/g, (...args) => { - const [, name, type] = args; - - params.push(data[name]); - - return `$${params.length}${type ?? ''}`; - }); - - return prisma.client.$executeRawUnsafe(query, ...params); -} - -async function findSnapshot( - websiteId: string, - urlPath: string, - viewportW: number, - viewportH: number, -): Promise { - if (clickhouse.enabled) { - return findClickhouseSnapshot(websiteId, urlPath, viewportW, viewportH); - } - - return findRelationalSnapshot(websiteId, urlPath, viewportW, viewportH); -} - -async function findRelationalSnapshot( - websiteId: string, - urlPath: string, - viewportW: number, - viewportH: number, -): Promise { - const rows = await prisma.rawQuery( - ` - select - snapshot_id as id, - website_id as "websiteId", - url_path as "urlPath", - viewport_w as "viewportW", - viewport_h as "viewportH", - page_w as "pageW", - page_h as "pageH", - status, - mime_type as "mimeType", - null as "objectKey", - image_size as "imageSize", - error, - image_data is not null as "hasImage", - updated_at as "updatedAt" - from heatmap_snapshot - where website_id = {{websiteId::uuid}} - and url_path = {{urlPath}} - and viewport_w = {{viewportW}} - and viewport_h = {{viewportH}} - limit 1 - `, - { websiteId, urlPath, viewportW, viewportH }, - 'findHeatmapSnapshot', - ); - - return rows?.[0] ?? null; -} - -async function findClickhouseSnapshot( - websiteId: string, - urlPath: string, - viewportW: number, - viewportH: number, -): Promise { - const rows = await clickhouse.rawQuery< - { - id: string; - websiteId: string; - urlPath: string; - viewportW: number; - viewportH: number; - pageW: number; - pageH: number; - status: number; - mimeType: string | null; - objectKey: string; - imageSize: number | null; - error: string | null; - createdAt: string; - }[] - >( - ` - select - snapshot_id as id, - website_id as websiteId, - url_path as urlPath, - viewport_w as viewportW, - viewport_h as viewportH, - page_w as pageW, - page_h as pageH, - status, - mime_type as mimeType, - object_key as objectKey, - image_size as imageSize, - error, - created_at as createdAt - from heatmap_snapshot - where website_id = {websiteId:UUID} - and url_path = {urlPath:String} - and viewport_w = {viewportW:UInt32} - and viewport_h = {viewportH:UInt32} - order by created_at desc - limit 1 - `, - { websiteId, urlPath, viewportW, viewportH }, - 'findHeatmapSnapshot', - ); - - const row = rows?.[0]; - - if (!row) { - return null; - } - - const status = Object.entries(CLICKHOUSE_SNAPSHOT_STATUS).find( - ([, value]) => value === row.status, - )?.[0]; - - if (!status) { - return null; - } - - return { - ...row, - status: SNAPSHOT_STATUS[status as keyof typeof SNAPSHOT_STATUS], - mimeType: row.mimeType || null, - objectKey: row.objectKey || null, - hasImage: row.status === CLICKHOUSE_SNAPSHOT_STATUS.ready && Boolean(row.objectKey), - updatedAt: row.createdAt, - }; -} - -function getSnapshotImageUrl(websiteId: string, snapshotId: string) { - return getApiUrl(`/websites/${websiteId}/heatmaps/snapshots/${snapshotId}`, { - apiUrl: process.env.API_URL, - basePath: process.env.BASE_PATH, - }); -} - -function mapSnapshot(websiteId: string, row: SnapshotRecord): HeatmapSnapshotImage { - return { - kind: 'image', - id: row.id, - imageUrl: - row.status === SNAPSHOT_STATUS.ready && row.hasImage - ? getSnapshotImageUrl(websiteId, row.id) - : null, - status: row.status, - mimeType: row.mimeType, - pageW: Number(row.pageW), - pageH: Number(row.pageH), - viewportW: Number(row.viewportW), - viewportH: Number(row.viewportH), - error: row.error, - }; -} - -function getFirstDomain(domain?: string | null) { - return domain?.split(',')[0]?.trim() || null; -} - -function getWebsiteOrigin(domain?: string | null) { - const host = getFirstDomain(domain); - - if (!host) { - return null; - } - - if (host.startsWith('http://') || host.startsWith('https://')) { - return new URL(host); - } - - const protocol = - host.startsWith('localhost') || host.startsWith('127.0.0.1') || host.startsWith('[::1]') - ? 'http' - : 'https'; - - return new URL(`${protocol}://${host}`); -} - -export function buildHeatmapPageUrl(domain: string | null | undefined, urlPath: string) { - try { - const origin = getWebsiteOrigin(domain); - - if (!origin) { - return null; - } - - return new URL(urlPath || '/', origin).toString(); - } catch { - return null; - } -} - -export function shouldSkipSnapshot(urlPath: string) { - // Internal Umami app routes cannot be rendered from the tracked website domain. - return urlPath.startsWith('/teams/'); -} - -async function upsertSnapshotRecord({ - id, - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - status, - mimeType, - imageData, - objectKey, - error, -}: { - id: string; - websiteId: string; - urlPath: string; - viewportW: number; - viewportH: number; - pageW: number; - pageH: number; - status: HeatmapSnapshotStatus; - mimeType: string | null; - imageData: Buffer | null; - objectKey?: string | null; - error: string | null; -}) { - if (clickhouse.enabled) { - return insertClickhouseSnapshotRecord({ - id, - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - status, - mimeType, - objectKey: objectKey ?? null, - imageSize: imageData?.byteLength ?? null, - error, - }); - } - - return upsertRelationalSnapshotRecord({ - id, - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - status, - mimeType, - imageData, - error, - }); -} - -async function upsertRelationalSnapshotRecord({ - id, - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - status, - mimeType, - imageData, - error, -}: { - id: string; - websiteId: string; - urlPath: string; - viewportW: number; - viewportH: number; - pageW: number; - pageH: number; - status: HeatmapSnapshotStatus; - mimeType: string | null; - imageData: Buffer | null; - error: string | null; -}) { - return rawExecute( - ` - insert into heatmap_snapshot ( - snapshot_id, - website_id, - url_path, - viewport_w, - viewport_h, - page_w, - page_h, - status, - mime_type, - image_data, - image_size, - error, - created_at, - updated_at - ) - values ( - {{id::uuid}}, - {{websiteId::uuid}}, - {{urlPath}}, - {{viewportW}}, - {{viewportH}}, - {{pageW}}, - {{pageH}}, - {{status}}, - {{mimeType}}, - {{imageData}}, - {{imageSize}}, - {{error}}, - now(), - now() - ) - on conflict (website_id, url_path, viewport_w, viewport_h) do update - set - page_w = excluded.page_w, - page_h = excluded.page_h, - status = excluded.status, - mime_type = excluded.mime_type, - image_data = excluded.image_data, - image_size = excluded.image_size, - error = excluded.error, - updated_at = now() - `, - { - id, - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - status, - mimeType, - imageData, - imageSize: imageData?.byteLength ?? null, - error, - }, - ); -} - -async function insertClickhouseSnapshotRecord({ - id, - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - status, - mimeType, - objectKey, - imageSize, - error, -}: { - id: string; - websiteId: string; - urlPath: string; - viewportW: number; - viewportH: number; - pageW: number; - pageH: number; - status: HeatmapSnapshotStatus; - mimeType: string | null; - objectKey: string | null; - imageSize: number | null; - error: string | null; -}) { - return clickhouse.insert('heatmap_snapshot', [ - { - snapshot_id: id, - website_id: websiteId, - url_path: urlPath, - viewport_w: viewportW, - viewport_h: viewportH, - page_w: pageW, - page_h: pageH, - status: CLICKHOUSE_SNAPSHOT_STATUS[status], - mime_type: mimeType || '', - object_key: objectKey || '', - image_size: imageSize, - error, - created_at: clickhouse.getUTCString(), - }, - ]); -} - -function getSnapshotObjectKey( - websiteId: string, - snapshotId: string, - viewportW: number, - viewportH: number, -) { - return `${websiteId}/${viewportW}x${viewportH}/${snapshotId}.png`; -} - -function getSnapshotErrorMessage(error: unknown) { - const message = - error instanceof Error - ? [error.name, error.message].filter(Boolean).join(': ') - : typeof error === 'string' - ? error - : SNAPSHOT_UNAVAILABLE_ERROR; - - return message.slice(0, SNAPSHOT_ERROR_MAX_LENGTH) || SNAPSHOT_UNAVAILABLE_ERROR; -} - -async function createSnapshotBrowser() { - const endpoint = process.env.PLAYWRIGHT_URL?.trim(); - - if (!endpoint) { - return null; - } - - const { chromium } = await import('playwright-core'); - return chromium.connect(endpoint); -} - -async function captureSnapshot( - url: string, - viewportW: number, - viewportH: number, -): Promise { - const browser = await createSnapshotBrowser(); - - if (!browser) { - throw new Error(SNAPSHOT_UNAVAILABLE_ERROR); - } - - const initialViewportW = viewportW; - - try { - const context = await browser.newContext({ - viewport: { width: initialViewportW, height: viewportH }, - screen: { width: initialViewportW, height: viewportH }, - deviceScaleFactor: SNAPSHOT_DEVICE_SCALE_FACTOR, - ignoreHTTPSErrors: true, - }); - - const page = await context.newPage(); - await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 }); - await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => undefined); - await page.waitForTimeout(500); - await warmLazyContent(page); - await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => undefined); - await page.waitForTimeout(300); - - let dimensions = await measurePage(page); - let currentWidth = initialViewportW; - let captureWidth = - dimensions.pageW > initialViewportW + 24 - ? Math.max(initialViewportW, dimensions.pageW) - : initialViewportW; - - for (let i = 0; i < 3; i++) { - if (captureWidth <= currentWidth) { - break; - } - - currentWidth = captureWidth; - - await page.setViewportSize({ - width: currentWidth, - height: viewportH, - }); - await page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => undefined); - await page.waitForTimeout(300); - - dimensions = await measurePage(page); - captureWidth = Math.max(currentWidth, dimensions.pageW); - } - - const imageData = Buffer.from(await page.screenshot({ fullPage: true, type: 'png' })); - - await context.close(); - - return { - imageData, - mimeType: 'image/png', - pageW: dimensions.pageW, - pageH: dimensions.pageH, - }; - } finally { - await browser.close(); - } -} - -export async function ensureHeatmapSnapshot({ - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, -}: EnsureHeatmapSnapshotOptions): Promise { - if (!urlPath || !viewportW || !viewportH || !pageW || !pageH) { - return null; - } - - if (shouldSkipSnapshot(urlPath)) { - return null; - } - - if (!process.env.PLAYWRIGHT_URL?.trim()) { - return null; - } - - const existing = await findSnapshot(websiteId, urlPath, viewportW, viewportH); - - if (existing?.status === SNAPSHOT_STATUS.ready && existing.hasImage) { - return mapSnapshot(websiteId, existing); - } - - const updatedAt = existing?.updatedAt ? new Date(existing.updatedAt) : null; - const ageMs = updatedAt ? Date.now() - updatedAt.getTime() : Number.POSITIVE_INFINITY; - - if (existing?.status === SNAPSHOT_STATUS.pending && ageMs < SNAPSHOT_PENDING_WINDOW_MS) { - return mapSnapshot(websiteId, existing); - } - - if (existing?.status === SNAPSHOT_STATUS.failed && ageMs < SNAPSHOT_RETRY_DELAY_MS) { - return mapSnapshot(websiteId, existing); - } - - const snapshotId = existing?.id ?? uuid(); - const website = await getWebsite(websiteId); - const captureUrl = buildHeatmapPageUrl(website?.domain, urlPath); - - if (!captureUrl) { - await upsertSnapshotRecord({ - id: snapshotId, - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - status: SNAPSHOT_STATUS.failed, - mimeType: null, - imageData: null, - error: SNAPSHOT_UNAVAILABLE_ERROR, - }); - - const failed = await findSnapshot(websiteId, urlPath, viewportW, viewportH); - - return failed ? mapSnapshot(websiteId, failed) : null; - } - - await upsertSnapshotRecord({ - id: snapshotId, - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - status: SNAPSHOT_STATUS.pending, - mimeType: null, - imageData: null, - error: null, - }); - - try { - const capture = await captureSnapshot(captureUrl, viewportW, viewportH); - const objectKey = clickhouse.enabled - ? getSnapshotObjectKey(websiteId, snapshotId, viewportW, viewportH) - : null; - - if (objectKey) { - await putHeatmapSnapshot(objectKey, capture.imageData, capture.mimeType); - } - - await upsertSnapshotRecord({ - id: snapshotId, - websiteId, - urlPath, - viewportW, - viewportH, - pageW: capture.pageW, - pageH: capture.pageH, - status: SNAPSHOT_STATUS.ready, - mimeType: capture.mimeType, - imageData: clickhouse.enabled ? null : capture.imageData, - objectKey, - error: null, - }); - } catch (error) { - console.error('Heatmap snapshot capture failed', { - captureUrl, - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - error: serializeError(error), - }); - - await upsertSnapshotRecord({ - id: snapshotId, - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - status: SNAPSHOT_STATUS.failed, - mimeType: null, - imageData: null, - error: getSnapshotErrorMessage(error), - }); - } - - const snapshot = await findSnapshot(websiteId, urlPath, viewportW, viewportH); - - return snapshot ? mapSnapshot(websiteId, snapshot) : null; -} - -export async function getHeatmapSnapshotImage( - websiteId: string, - snapshotId: string, -): Promise<{ mimeType: string; imageData: Buffer } | null> { - if (clickhouse.enabled) { - const rows = await clickhouse.rawQuery<{ mimeType: string; objectKey: string }[]>( - ` - select - mime_type as mimeType, - object_key as objectKey - from heatmap_snapshot - where snapshot_id = {snapshotId:UUID} - and website_id = {websiteId:UUID} - and status = {status:UInt8} - and object_key != '' - order by created_at desc - limit 1 - `, - { - websiteId, - snapshotId, - status: CLICKHOUSE_SNAPSHOT_STATUS.ready, - }, - 'getHeatmapSnapshotImage', - ); - - const row = rows?.[0]; - - if (!row?.objectKey) { - return null; - } - - return getHeatmapSnapshot(row.objectKey); - } - - const rows = await prisma.rawQuery( - ` - select - mime_type as "mimeType", - image_data as "imageData" - from heatmap_snapshot - where snapshot_id = {{snapshotId::uuid}} - and website_id = {{websiteId::uuid}} - and status = 'ready' - and image_data is not null - limit 1 - `, - { websiteId, snapshotId }, - 'getHeatmapSnapshotImage', - ); - - const row = rows?.[0]; - - if (!row?.imageData || !row?.mimeType) { - return null; - } - - return { - mimeType: row.mimeType, - imageData: Buffer.from(row.imageData), - }; -} diff --git a/src/queries/sql/heatmap/extractHeatmapEvents.ts b/src/queries/sql/heatmap/extractHeatmapEvents.ts index cb8d65c79..41e0aa2da 100644 --- a/src/queries/sql/heatmap/extractHeatmapEvents.ts +++ b/src/queries/sql/heatmap/extractHeatmapEvents.ts @@ -9,7 +9,6 @@ const RRWEB_MOUSE_CLICK = 2; export interface ExtractedHeatmapEvent { eventType: number; - nodeId: number | null; x: number | null; y: number | null; viewportW: number | null; @@ -18,9 +17,6 @@ export interface ExtractedHeatmapEvent { scrollPct: number | null; urlPath: string; createdAt: Date; - replayChunkIndex: number | null; - replayEventIndex: number | null; - replayTimeMs: number | null; } interface ExtractHeatmapEventOptions { @@ -36,10 +32,7 @@ function safePathname(href: unknown): string | null { } } -export function extractHeatmapEvents( - events: any[], - { chunkIndex }: ExtractHeatmapEventOptions = {}, -): ExtractedHeatmapEvent[] { +export function extractHeatmapEvents(events: any[], _options: ExtractHeatmapEventOptions = {}) { if (!Array.isArray(events) || events.length === 0) return []; let urlPath: string | null = null; @@ -47,7 +40,7 @@ export function extractHeatmapEvents( let viewportH: number | null = null; const out: ExtractedHeatmapEvent[] = []; - for (const [eventIndex, ev] of events.entries()) { + for (const ev of events) { if (!ev || typeof ev !== 'object') continue; const replayTimeMs = typeof ev.timestamp === 'number' && Number.isFinite(ev.timestamp) ? ev.timestamp : null; @@ -73,7 +66,6 @@ export function extractHeatmapEvents( if (path === null) continue; out.push({ eventType: HEATMAP_EVENT_TYPE.scroll, - nodeId: null, x: null, y: null, viewportW: typeof p.viewportW === 'number' ? p.viewportW : viewportW, @@ -85,9 +77,6 @@ export function extractHeatmapEvents( : null, urlPath: path, createdAt: new Date(replayTimeMs ?? Date.now()), - replayChunkIndex: chunkIndex ?? null, - replayEventIndex: eventIndex, - replayTimeMs, }); } continue; @@ -110,7 +99,6 @@ export function extractHeatmapEvents( ) { 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, @@ -119,9 +107,6 @@ export function extractHeatmapEvents( scrollPct: null, urlPath, createdAt: new Date(replayTimeMs ?? Date.now()), - replayChunkIndex: chunkIndex ?? null, - replayEventIndex: eventIndex, - replayTimeMs, }); } } diff --git a/src/queries/sql/heatmap/getHeatmap.ts b/src/queries/sql/heatmap/getHeatmap.ts index 932a7c294..f3283506a 100644 --- a/src/queries/sql/heatmap/getHeatmap.ts +++ b/src/queries/sql/heatmap/getHeatmap.ts @@ -5,12 +5,6 @@ import { filtersObjectToArray } from '@/lib/params'; import prisma from '@/lib/prisma'; import type { QueryFilters } from '@/lib/types'; import { getWebsite } from '@/queries/prisma'; -import { - buildHeatmapPageUrl, - ensureHeatmapSnapshot, - type HeatmapSnapshotImage, - shouldSkipSnapshot, -} from './ensureHeatmapSnapshot'; const FUNCTION_NAME = 'getHeatmap'; @@ -32,7 +26,6 @@ export interface HeatmapPage { } export interface HeatmapPoint { - nodeId: number | null; x: number; y: number; pageX: number; @@ -63,7 +56,7 @@ export interface HeatmapSnapshotIframe { viewportH: number; } -export type HeatmapSnapshot = HeatmapSnapshotImage | HeatmapSnapshotIframe; +export type HeatmapSnapshot = HeatmapSnapshotIframe; export interface HeatmapResult { mode: HeatmapMode; @@ -234,7 +227,6 @@ async function relationalQuery( const rawPoints: HeatmapPoint[] = await rawQuery( ` select - h.node_id as "nodeId", h.x, h.y, h.page_x as "pageX", @@ -260,7 +252,6 @@ async function relationalQuery( and h.viewport_w is not null and h.viewport_h is not null group by - h.node_id, h.x, h.y, h.page_x, @@ -441,7 +432,6 @@ async function clickhouseQuery( const pointRows = await rawQuery< { - nodeId: number | null; x: number; y: number; pageX: number; @@ -455,7 +445,6 @@ async function clickhouseQuery( >( ` select - h.node_id as nodeId, h.x, h.y, h.page_x as pageX, @@ -481,7 +470,6 @@ async function clickhouseQuery( and h.viewport_w is not null and h.viewport_h is not null group by - h.node_id, h.x, h.y, h.page_x, @@ -498,7 +486,6 @@ async function clickhouseQuery( ); const points: HeatmapPoint[] = pointRows.map(p => ({ - nodeId: p.nodeId === null || p.nodeId === undefined ? null : Number(p.nodeId), x: Number(p.x), y: Number(p.y), pageX: Number(p.pageX), @@ -549,19 +536,6 @@ async function resolveHeatmapSnapshot({ pageW: number | null; pageH: number | null; }): Promise { - const imageSnapshot = await ensureHeatmapSnapshot({ - websiteId, - urlPath, - viewportW, - viewportH, - pageW, - pageH, - }); - - if (imageSnapshot?.status === 'ready' && imageSnapshot.imageUrl) { - return imageSnapshot; - } - return getIframeSnapshot({ websiteId, urlPath, @@ -612,6 +586,48 @@ async function getIframeSnapshot({ }; } +function getFirstDomain(domain?: string | null) { + return domain?.split(',')[0]?.trim() || null; +} + +function getWebsiteOrigin(domain?: string | null) { + const host = getFirstDomain(domain); + + if (!host) { + return null; + } + + if (host.startsWith('http://') || host.startsWith('https://')) { + return new URL(host); + } + + const protocol = + host.startsWith('localhost') || host.startsWith('127.0.0.1') || host.startsWith('[::1]') + ? 'http' + : 'https'; + + return new URL(`${protocol}://${host}`); +} + +function buildHeatmapPageUrl(domain: string | null | undefined, urlPath: string) { + try { + const origin = getWebsiteOrigin(domain); + + if (!origin) { + return null; + } + + return new URL(urlPath || '/', origin).toString(); + } catch { + return null; + } +} + +function shouldSkipSnapshot(urlPath: string) { + // Internal Umami app routes cannot be rendered from the tracked website domain. + return urlPath.startsWith('/teams/'); +} + function pickSnapshotViewport( points: HeatmapPoint[], ): { width: number; height: number; pageW: number; pageH: number } | null { diff --git a/src/queries/sql/heatmap/saveHeatmapEvents.ts b/src/queries/sql/heatmap/saveHeatmapEvents.ts index 78988d0a5..60277bc78 100644 --- a/src/queries/sql/heatmap/saveHeatmapEvents.ts +++ b/src/queries/sql/heatmap/saveHeatmapEvents.ts @@ -10,7 +10,6 @@ export interface HeatmapEventRow { visitId: string; urlPath: string; eventType: number; - nodeId: number | null; x: number | null; y: number | null; pageX: number | null; @@ -21,9 +20,6 @@ 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[]) { @@ -31,7 +27,6 @@ export async function saveHeatmapEvents(rows: HeatmapEventRow[]) { const normalizedRows = rows.map(r => ({ ...r, - nodeId: toInt(r.nodeId), x: toInt(r.x), y: toInt(r.y), pageX: toInt(r.pageX), @@ -70,7 +65,6 @@ async function relationalQuery(rows: HeatmapEventRow[]) { visitId: r.visitId, urlPath: r.urlPath, eventType: r.eventType, - nodeId: r.nodeId, x: r.x, y: r.y, pageX: r.pageX, @@ -81,9 +75,6 @@ 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, }); } @@ -99,7 +90,6 @@ async function clickhouseQuery(rows: HeatmapEventRow[]) { visit_id: r.visitId, url_path: r.urlPath, event_type: r.eventType, - node_id: r.nodeId, x: r.x, y: r.y, page_x: r.pageX, @@ -110,9 +100,6 @@ 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) {