From f3ce0e64cadc7245e8f83e2ee880febcf8252309 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 17 Jun 2026 10:02:12 -0700 Subject: [PATCH] typescript fixes, replayer close error, show page toggle fix. --- .../console/[websiteId]/TestConsolePage.tsx | 4 +-- src/app/(main)/links/LinkEditButton.tsx | 1 - src/app/(main)/pixels/PixelEditButton.tsx | 1 - .../(reports)/heatmaps/Heatmap.tsx | 8 +++--- .../[websiteId]/realtime/RealtimePaths.tsx | 4 +-- .../realtime/RealtimeReferrers.tsx | 4 +-- .../replays/[replayId]/ReplayPlayer.tsx | 19 +++++++++++-- src/app/api/auth/logout/route.test.ts | 2 +- src/components/common/CopyButton.tsx | 2 +- src/components/common/ErrorBoundary.tsx | 4 +-- src/components/common/FilterLink.tsx | 5 ++-- src/lib/db.ts | 2 +- src/lib/params.ts | 6 ++++- src/lib/react.ts | 6 +++-- src/tracker/index.d.ts | 27 ++++++++++++++++--- 15 files changed, 67 insertions(+), 28 deletions(-) diff --git a/src/app/(main)/console/[websiteId]/TestConsolePage.tsx b/src/app/(main)/console/[websiteId]/TestConsolePage.tsx index 575685dec..26b85cf42 100644 --- a/src/app/(main)/console/[websiteId]/TestConsolePage.tsx +++ b/src/app/(main)/console/[websiteId]/TestConsolePage.tsx @@ -23,7 +23,7 @@ export function TestConsolePage({ websiteId }: { websiteId: string }) { test: 'test-data', boolean: true, booleanError: 'true', - time: new Date(), + time: new Date().toISOString(), user: `user${Math.round(Math.random() * 10)}`, number: 1, number2: Math.random() * 100, @@ -87,7 +87,7 @@ export function TestConsolePage({ websiteId }: { websiteId: string }) { test: 'test-data', boolean: true, booleanError: 'true', - time: new Date(), + time: new Date().toISOString(), time2: new Date().toISOString(), nested: { test: 'test-data', diff --git a/src/app/(main)/links/LinkEditButton.tsx b/src/app/(main)/links/LinkEditButton.tsx index 9c5d23330..b1358a564 100644 --- a/src/app/(main)/links/LinkEditButton.tsx +++ b/src/app/(main)/links/LinkEditButton.tsx @@ -10,7 +10,6 @@ export function LinkEditButton({ linkId }: { linkId: string }) { return ( diff --git a/src/app/(main)/pixels/PixelEditButton.tsx b/src/app/(main)/pixels/PixelEditButton.tsx index d96e8ea21..18f50aaae 100644 --- a/src/app/(main)/pixels/PixelEditButton.tsx +++ b/src/app/(main)/pixels/PixelEditButton.tsx @@ -10,7 +10,6 @@ export function PixelEditButton({ pixelId }: { pixelId: string }) { return ( diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx index 62cdc51d1..44f581568 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx @@ -269,8 +269,8 @@ function ClickHeatmapView({ const hasSnapshot = Boolean(snapshot); useEffect(() => { - setSnapshotReady(!(showPage && hasSnapshot)); - }, [hasSnapshot, showPage, snapshot?.id]); + setSnapshotReady(!hasSnapshot); + }, [hasSnapshot, snapshot?.id]); const overlayGutter = Math.max(48, Math.round((viewport?.width ?? 1920) * 0.04)); const maxPointX = visible.reduce((max, point) => Math.max(max, point.pageX), 0); const maxPointY = visible.reduce((max, point) => Math.max(max, point.pageY), 0); @@ -425,8 +425,8 @@ function ScrollHeatmapView({ const hasSnapshot = Boolean(snapshot); useEffect(() => { - setSnapshotReady(!(showPage && hasSnapshot)); - }, [hasSnapshot, showPage, snapshot?.id]); + setSnapshotReady(!hasSnapshot); + }, [hasSnapshot, snapshot?.id]); const { buckets = [], totalSessions = 0, diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx index e69d9e645..2a91e01ee 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimePaths.tsx @@ -1,4 +1,4 @@ -import thenby from 'thenby'; +import { firstBy } from 'thenby'; import { useMessages, useWebsite } from '@/components/hooks'; import { ListTable } from '@/components/metrics/ListTable'; import { percentFilter } from '@/lib/filters'; @@ -26,7 +26,7 @@ export function RealtimePaths({ data }: { data: any }) { y: urls[key], }; }) - .sort(thenby.firstBy('y', -1)) + .sort(firstBy('y', -1)) .slice(0, limit), ); diff --git a/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx b/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx index fa2a56b9f..e2d2305b0 100644 --- a/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx +++ b/src/app/(main)/websites/[websiteId]/realtime/RealtimeReferrers.tsx @@ -1,7 +1,7 @@ -import thenby from 'thenby'; import { useMessages, useWebsite } from '@/components/hooks'; import { ListTable } from '@/components/metrics/ListTable'; import { percentFilter } from '@/lib/filters'; +import { firstBy } from 'thenby'; export function RealtimeReferrers({ data }: { data: any }) { const website = useWebsite(); @@ -26,7 +26,7 @@ export function RealtimeReferrers({ data }: { data: any }) { y: referrers[key], }; }) - .sort(thenby.firstBy('y', -1)) + .sort(firstBy('y', -1)) .slice(0, limit), ); diff --git a/src/app/(main)/websites/[websiteId]/replays/[replayId]/ReplayPlayer.tsx b/src/app/(main)/websites/[websiteId]/replays/[replayId]/ReplayPlayer.tsx index 960f8e4a0..b413b9c06 100644 --- a/src/app/(main)/websites/[websiteId]/replays/[replayId]/ReplayPlayer.tsx +++ b/src/app/(main)/websites/[websiteId]/replays/[replayId]/ReplayPlayer.tsx @@ -8,6 +8,21 @@ import 'rrweb-player/dist/style.css'; const DEFAULT_REPLAY_ASPECT_RATIO = 9 / 16; +function destroyReplayPlayer(player: { $destroy?: () => void } | null) { + if (!player) { + return; + } + + try { + player.$destroy?.(); + } catch (error) { + // rrweb-player alpha teardown can throw after it loses its inner replayer instance. + if (!(error instanceof TypeError) || !error.message.includes('pause is not a function')) { + throw error; + } + } +} + export function ReplayPlayer({ events }: { events: any[] }) { const playerRootRef = useRef(null); const playerRef = useRef(null); @@ -34,7 +49,7 @@ export function ReplayPlayer({ events }: { events: any[] }) { setPlayerError(false); if (playerRef.current) { - playerRef.current.$destroy?.(); + destroyReplayPlayer(playerRef.current); playerRef.current = null; } @@ -89,7 +104,7 @@ export function ReplayPlayer({ events }: { events: any[] }) { cancelled = true; if (playerRef.current) { - playerRef.current.$destroy?.(); + destroyReplayPlayer(playerRef.current); playerRef.current = null; } diff --git a/src/app/api/auth/logout/route.test.ts b/src/app/api/auth/logout/route.test.ts index c9bcfd8b4..d90df11da 100644 --- a/src/app/api/auth/logout/route.test.ts +++ b/src/app/api/auth/logout/route.test.ts @@ -20,7 +20,7 @@ vi.mock('@/lib/response', () => ({ ok: () => new Response(null, { status: 200 }), })); -const redisMock = redis as { +const redisMock = redis as unknown as { enabled: boolean; client: { del: ReturnType; diff --git a/src/components/common/CopyButton.tsx b/src/components/common/CopyButton.tsx index f719c229c..e00cc3e2e 100644 --- a/src/components/common/CopyButton.tsx +++ b/src/components/common/CopyButton.tsx @@ -32,7 +32,7 @@ export function CopyButton({ value, label = 'Copy' }: { value: string; label?: s }; return ( - ); diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx index e70c193a3..39803d80e 100644 --- a/src/components/common/ErrorBoundary.tsx +++ b/src/components/common/ErrorBoundary.tsx @@ -3,9 +3,9 @@ import type { ErrorInfo, ReactNode } from 'react'; import { ErrorBoundary as Boundary } from 'react-error-boundary'; import { useMessages } from '@/components/hooks'; -const logError = (error: Error, info: ErrorInfo) => { +const logError = (error: unknown, info: ErrorInfo) => { // eslint-disable-next-line no-console - console.error(error, info.componentStack); + console.error(error instanceof Error ? error : new Error(String(error)), info.componentStack); }; export function ErrorBoundary({ children }: { children: ReactNode }) { diff --git a/src/components/common/FilterLink.tsx b/src/components/common/FilterLink.tsx index ad5ea6722..7c37975b1 100644 --- a/src/components/common/FilterLink.tsx +++ b/src/components/common/FilterLink.tsx @@ -23,15 +23,14 @@ export function FilterLink({ type, value, label, externalUrl, icon }: FilterLink setShowLink(true)} onMouseOut={() => setShowLink(false)} > {icon} - {!value && `(${label || t(labels.unknown)})`} + {!value && ({label || t(labels.unknown)})} {value && ( - + {label || value} diff --git a/src/lib/db.ts b/src/lib/db.ts index 7b6e8368b..f363a9df9 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -5,7 +5,7 @@ export const KAFKA = 'kafka'; export const KAFKA_PRODUCER = 'kafka-producer'; // Fixes issue with converting bigint values -BigInt.prototype.toJSON = function () { +(BigInt.prototype as unknown as { toJSON(): number }).toJSON = function () { return Number(this); }; diff --git a/src/lib/params.ts b/src/lib/params.ts index 17341fd53..f924ee30a 100644 --- a/src/lib/params.ts +++ b/src/lib/params.ts @@ -4,6 +4,10 @@ import type { Filter, Operator, PropertyFilter, QueryFilters, QueryOptions } fro const VALID_OPERATORS: Operator[] = Object.values(OPERATORS); const VALID_EVENT_DATA_TYPES = Object.values(DATA_TYPE); +function isValidEventDataType(value: number): value is (typeof VALID_EVENT_DATA_TYPES)[number] { + return VALID_EVENT_DATA_TYPES.includes(value as (typeof VALID_EVENT_DATA_TYPES)[number]); +} + function resolveOperator(value?: string): Operator | undefined { if (!value) { return undefined; @@ -126,7 +130,7 @@ export function parsePropertyFilters( if ( !operator || - (explicitDataType !== undefined && !VALID_EVENT_DATA_TYPES.includes(explicitDataType)) + (explicitDataType !== undefined && !isValidEventDataType(explicitDataType)) ) { return []; } diff --git a/src/lib/react.ts b/src/lib/react.ts index 668cdf1fb..c1cf1414a 100644 --- a/src/lib/react.ts +++ b/src/lib/react.ts @@ -8,9 +8,11 @@ import { type ReactNode, } from 'react'; +type ReactElementWithChildren = ReactElement<{ children?: ReactNode }>; + export function getFragmentChildren(children: ReactNode) { - return (children as ReactElement)?.type === Fragment - ? (children as ReactElement).props.children + return (children as ReactElementWithChildren)?.type === Fragment + ? (children as ReactElementWithChildren).props.children : children; } diff --git a/src/tracker/index.d.ts b/src/tracker/index.d.ts index 32fbee97f..46c5d026d 100644 --- a/src/tracker/index.d.ts +++ b/src/tracker/index.d.ts @@ -66,7 +66,7 @@ export type WithRequired = T & { [P in K]-?: T[P] }; * - Objects have a max of 50 properties. Arrays are considered 1 property. */ export interface EventData { - [key: string]: number | string | EventData | number[] | string[] | EventData[]; + [key: string]: boolean | number | string | EventData | boolean[] | number[] | string[] | EventData[]; } export type EventProperties = { @@ -146,8 +146,29 @@ export type UmamiTracker = { */ (eventFunction: CustomEventFunction): Promise; }; + identify: { + /** + * Identify a visitor with optional associated data. + * + * @example ``` + * umami.identify('user-123', { plan: 'pro' }); + * ``` + */ + (id: string, data?: EventData): Promise; + + /** + * Associate data with the current visitor without setting an identity string. + * + * @example ``` + * umami.identify({ plan: 'pro' }); + * ``` + */ + (data: EventData): Promise; + }; }; -export interface Window { - umami: UmamiTracker; +declare global { + interface Window { + umami: UmamiTracker; + } }