diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css index 4f19c45ca..689061ba4 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css +++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css @@ -84,10 +84,6 @@ white-space: nowrap; } -.snapshotMessage { - margin-top: -4px; -} - .scrollBand { position: absolute; left: 0; @@ -172,9 +168,13 @@ -webkit-user-drag: none; } -.snapshotFrame { +.snapshotIframe { + display: block; width: 100%; height: 100%; + border: 0; + background: var(--surface-base); + pointer-events: none; } .overlay { diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx index f94790a02..1101ee640 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx @@ -1,8 +1,8 @@ 'use client'; import { Column, Grid, Heading, Loading, Row, Switch, Text } from '@umami/react-zen'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { LoadingPanel } from '@/components/common/LoadingPanel'; -import { useReplayQuery, useResultQuery } from '@/components/hooks'; +import { useResultQuery } from '@/components/hooks'; import { getClientAuthToken } from '@/lib/client'; import { formatLongNumber } from '@/lib/format'; import type { HeatmapMode, HeatmapPoint, HeatmapResult, HeatmapSnapshot } from '@/queries/sql'; @@ -10,7 +10,6 @@ import styles from './Heatmap.module.css'; const CLICK_EDGE_PERCENT = 1.5; const SCROLL_BUCKET_SIZE = 10; -const SNAPSHOT_UNAVAILABLE_ERROR = 'Page screenshot unavailable.'; interface ViewportBucket { width: number; @@ -111,7 +110,6 @@ export function Heatmap({ websiteId, urlPath, onUrlPathChange, mode, search }: H {urlPath ? ( mode === 'scroll' ? ( ) : ( - {showPage && snapshot?.kind === 'image' && snapshot.status === 'failed' && ( - - {SNAPSHOT_UNAVAILABLE_ERROR} - - )} -
} {shouldRenderSnapshot && snapshot && ( )}
@@ -410,13 +395,11 @@ function ClickHeatmapView({ } function ScrollHeatmapView({ - websiteId, urlPath, scroll, snapshot, isLoading, }: { - websiteId: string; urlPath: string; scroll: HeatmapResult['scroll'] | undefined; snapshot: HeatmapSnapshot | null; @@ -444,7 +427,7 @@ function ScrollHeatmapView({ const renderHeight = snapshot?.pageH ?? baseHeight; const hasMeasuredWidth = Boolean(snapshot?.pageW || pageW); const canvasWidth = hasMeasuredWidth - ? snapshot?.kind === 'replay' + ? snapshot?.kind === 'iframe' ? `${renderWidth}px` : `min(100%, ${renderWidth}px)` : '100%'; @@ -501,14 +484,8 @@ function ScrollHeatmapView({ )} - {showPage && snapshot?.kind === 'image' && snapshot.status === 'failed' && ( - - {SNAPSHOT_UNAVAILABLE_ERROR} - - )} -
} {shouldRenderSnapshot && snapshot && ( )} {showOverlay && ( @@ -578,28 +551,33 @@ function ScrollHeatmapView({ } function SnapshotPreview({ - websiteId, snapshot, onReady, }: { - websiteId: string; snapshot: HeatmapSnapshot; onReady: () => void; }) { - if (snapshot.kind === 'replay') { - return ; + if (snapshot.kind === 'iframe') { + return ; } return ; } -function SnapshotImage({ snapshot, onReady }: { snapshot: HeatmapSnapshot; onReady: () => void }) { +function SnapshotImage({ + snapshot, + onReady, +}: { + snapshot: Extract; + onReady: () => void; +}) { const [src, setSrc] = useState(null); - const imageUrl = snapshot.kind === 'image' ? snapshot.imageUrl : null; + const imageUrl = snapshot.imageUrl; useEffect(() => { - if (snapshot.kind !== 'image' || !imageUrl) { + if (!imageUrl) { setSrc(null); + onReady(); return; } @@ -636,7 +614,7 @@ function SnapshotImage({ snapshot, onReady }: { snapshot: HeatmapSnapshot; onRea URL.revokeObjectURL(objectUrl); } }; - }, [imageUrl, onReady, snapshot.id, snapshot.kind]); + }, [imageUrl, onReady, snapshot.id]); const handleLoad = useCallback(() => onReady(), [onReady]); @@ -653,91 +631,45 @@ function SnapshotImage({ snapshot, onReady }: { snapshot: HeatmapSnapshot; onRea ); } -function ReplaySnapshot({ - websiteId, +function IframeSnapshot({ snapshot, onReady, }: { - websiteId: string; - snapshot: Extract; + snapshot: Extract; onReady: () => void; }) { - const containerRef = useRef(null); - const replayerRef = useRef(null); - const { data: replay, isLoading } = useReplayQuery(websiteId, snapshot.replayId, { - until: snapshot.replayTimeMs ?? undefined, - chunkIndex: snapshot.chunkIndex, - eventIndex: snapshot.eventIndex, - }); + const [available, setAvailable] = useState(true); useEffect(() => { - if (isLoading) { - return; - } + setAvailable(true); - if (!replay?.events?.length || !containerRef.current) { - onReady(); - return; - } + const readyTimer = window.setTimeout(() => onReady(), 1500); - let cancelled = false; + return () => window.clearTimeout(readyTimer); + }, [onReady, snapshot.id]); - import('rrweb') - .then(mod => { - if (cancelled || !containerRef.current) { - return; - } + const handleLoad = useCallback(() => onReady(), [onReady]); + const handleError = useCallback(() => { + setAvailable(false); + onReady(); + }, [onReady]); - containerRef.current.innerHTML = ''; - - const replayer = new mod.Replayer(replay.events, { - root: containerRef.current, - loadTimeout: 0, - mouseTail: false, - pauseAnimation: true, - showWarning: false, - }); - - replayerRef.current = replayer; - - const firstTimestamp = Number(replay.events[0]?.timestamp ?? 0); - const lastTimestamp = Number(replay.events[replay.events.length - 1]?.timestamp ?? 0); - const timeOffset = Math.max(0, lastTimestamp - firstTimestamp); - - replayer.pause(timeOffset); - replayer.wrapper.style.width = '100%'; - replayer.wrapper.style.height = '100%'; - replayer.wrapper.style.pointerEvents = 'none'; - replayer.wrapper.style.overflow = 'hidden'; - replayer.iframe.style.display = 'block'; - replayer.iframe.style.width = '100%'; - replayer.iframe.style.height = '100%'; - replayer.iframe.style.border = '0'; - const replayMouse = replayer.wrapper.querySelector('.replayer-mouse'); - const replayMouseTail = replayer.wrapper.querySelector( - '.replayer-mouse-tail', - ); - if (replayMouse) { - replayMouse.style.display = 'none'; - } - if (replayMouseTail) { - replayMouseTail.style.display = 'none'; - } - - onReady(); - }) - .catch(() => onReady()); - - return () => { - cancelled = true; - replayerRef.current?.destroy?.(); - replayerRef.current = null; - }; - }, [isLoading, onReady, replay?.events, snapshot.id]); + if (!available) { + return null; + } return (
-
+