diff --git a/.changeset/four-panthers-fly.md b/.changeset/four-panthers-fly.md index 5d7a610438..6bb92a03f8 100644 --- a/.changeset/four-panthers-fly.md +++ b/.changeset/four-panthers-fly.md @@ -2,4 +2,4 @@ "@rrweb/web-extension": patch --- -refactor: improved tab recording to improve stability +web-extension: improve recording stability across tabs and enable session import diff --git a/packages/web-extension/src/pages/SessionList.tsx b/packages/web-extension/src/pages/SessionList.tsx index d285932029..e3ec6bd545 100644 --- a/packages/web-extension/src/pages/SessionList.tsx +++ b/packages/web-extension/src/pages/SessionList.tsx @@ -1,4 +1,5 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { nanoid } from 'nanoid'; import { chakra, Table, @@ -15,6 +16,7 @@ import { Spacer, IconButton, Select, + useToast, Input, Divider, } from '@chakra-ui/react'; @@ -29,9 +31,15 @@ import { } from '@tanstack/react-table'; import { VscTriangleDown, VscTriangleUp } from 'react-icons/vsc'; import { useNavigate } from 'react-router-dom'; +import type { eventWithTime } from 'rrweb'; import { type Session, EventName } from '~/types'; import Channel from '~/utils/channel'; -import { deleteSessions, getAllSessions, downloadSessions } from '~/utils/storage'; +import { + deleteSessions, + getAllSessions, + downloadSessions, + saveSession, +} from '~/utils/storage'; import { FiChevronLeft, FiChevronRight, @@ -43,8 +51,10 @@ const columnHelper = createColumnHelper(); const channel = new Channel(); export function SessionList() { - const [sessions, setSessions] = useState([]); const navigate = useNavigate(); + const toast = useToast(); + const fileInputRef = useRef(null); + const [sessions, setSessions] = useState([]); const [sorting, setSorting] = useState([ { id: 'createTimestamp', @@ -145,8 +155,63 @@ export function SessionList() { }); }, []); + const handleFileUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (e) => { + try { + const content = e.target?.result as string; + const data = JSON.parse(content) as { + session: Session; + events: eventWithTime[]; + }; + const id = nanoid(); + data.session.id = id; + await saveSession(data.session, data.events); + toast({ + title: 'Session imported', + description: 'The session was successfully imported.', + status: 'success', + duration: 3000, + isClosable: true, + }); + await updateSessions(); + } catch (error) { + console.error('Error uploading file:', error); + toast({ + title: 'Error importing session', + description: (error as Error).message, + status: 'error', + duration: 3000, + isClosable: true, + }); + } + }; + reader.readAsText(file); + }; + return ( <> + + + + @@ -318,7 +383,9 @@ export function SessionList() { onClick={() => { const selectedRows = table.getSelectedRowModel().flatRows; if (selectedRows.length === 0) return; - void downloadSessions(selectedRows.map((row) => row.original.id)); + void downloadSessions( + selectedRows.map((row) => row.original.id), + ); }} > Download diff --git a/packages/web-extension/src/utils/recording.ts b/packages/web-extension/src/utils/recording.ts deleted file mode 100644 index 6a46bd1f99..0000000000 --- a/packages/web-extension/src/utils/recording.ts +++ /dev/null @@ -1,90 +0,0 @@ -import Browser from 'webextension-polyfill'; -import type { eventWithTime } from '@rrweb/types'; - -import { - type LocalData, - LocalDataKey, - RecorderStatus, - type RecordStartedMessage, - type RecordStoppedMessage, - ServiceName, -} from '~/types'; -import type Channel from './channel'; -import { isFirefox } from '.'; - -/** - * Some commonly used functions for session recording. - */ - -// Pause recording. -export async function pauseRecording( - channel: Channel, - newStatus: RecorderStatus, - status?: LocalData[LocalDataKey.recorderStatus], -) { - if (!status) - status = (await Browser.storage.local.get(LocalDataKey.recorderStatus))[ - LocalDataKey.recorderStatus - ] as LocalData[LocalDataKey.recorderStatus]; - const { startTimestamp, activeTabId } = status; - const stopResponse = (await channel.requestToTab( - activeTabId, - ServiceName.PauseRecord, - {}, - )) as RecordStoppedMessage; - if (!stopResponse) return; - const statusData: LocalData[LocalDataKey.recorderStatus] = { - status: newStatus, - activeTabId, - startTimestamp, - pausedTimestamp: stopResponse.endTimestamp, - }; - await Browser.storage.local.set({ - [LocalDataKey.recorderStatus]: statusData, - }); - return { - status: statusData, - bufferedEvents: stopResponse.events, - }; -} - -// Resume recording after change to a new tab. -export async function resumeRecording( - channel: Channel, - newTabId: number, - status?: LocalData[LocalDataKey.recorderStatus], - bufferedEvents?: eventWithTime[], -) { - if (!status) - status = (await Browser.storage.local.get(LocalDataKey.recorderStatus))[ - LocalDataKey.recorderStatus - ] as LocalData[LocalDataKey.recorderStatus]; - if (!bufferedEvents) - bufferedEvents = ( - (await Browser.storage.local.get( - LocalDataKey.bufferedEvents, - )) as LocalData - )[LocalDataKey.bufferedEvents]; - const { startTimestamp, pausedTimestamp } = status; - // On Firefox, the new tab is not communicable immediately after it is created. - if (isFirefox()) await new Promise((r) => setTimeout(r, 50)); - const startResponse = (await channel.requestToTab( - newTabId, - ServiceName.ResumeRecord, - { events: bufferedEvents, pausedTimestamp }, - )) as RecordStartedMessage; - if (!startResponse) return; - const pausedTime = pausedTimestamp - ? startResponse.startTimestamp - pausedTimestamp - : 0; - const statusData: LocalData[LocalDataKey.recorderStatus] = { - status: RecorderStatus.RECORDING, - activeTabId: newTabId, - startTimestamp: - (startTimestamp || bufferedEvents[0].timestamp) + pausedTime, - }; - await Browser.storage.local.set({ - [LocalDataKey.recorderStatus]: statusData, - }); - return statusData; -}