* feat: add rrweb web-extension package * refactor: make the extension suitable for manifest v3 * update tsconfig.json * use version_name rather than recorder_version in manifest.json * update manifest.json * enable to keep recording after changing tabs * enable to record between tabs and urls * fix CI error * try to fix CI error * feat: add pause and resume buttons * feat: add a link to new session after recording * improve session list * refactor: migrate session storage from chrome local storage to indexedDB * feat: add pagination to session list * fix: multiple recorders are started after pausing and resuming process * fix: can't stop recording on firefox browser * update type import of 'eventWithTime' * fix CI error * doc: add readme * Apply suggestions from Justin's code review Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com> * refactor: make use of webNavigation API to implement recording consistent during page navigation * fix firefox compatibility issue and add title to pages * add mouseleave listener to enhance the recording liability * fix firefox compatibility issue and improve the experience of recording resume after closing tabs * update tsconfig * upgrade vite-plugin-web-extension config to fix some bugs on facebook web page * update import links * refactor: cross tab recording mechanism apply Justin's suggestion * refactor: slipt util/index.ts into multiple files * implement cross-origin iframe recording * fix: regression of issue: ShadowHost can't be a string (issue 941) * refactor shadow dom recording to make tests cover key code * Apply formatting changes * increase the node memory limitation to avoid CI failure * Create lovely-pears-cross.md * Apply formatting changes * Update packages/web-extension/package.json * Update .changeset/lovely-pears-cross.md * update change logs * delete duplicated property --------- Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>
163 lines
5.5 KiB
TypeScript
163 lines
5.5 KiB
TypeScript
import Browser from 'webextension-polyfill';
|
|
import type { eventWithTime } from '@rrweb/types';
|
|
import Channel from '~/utils/channel';
|
|
import {
|
|
LocalData,
|
|
LocalDataKey,
|
|
RecorderStatus,
|
|
Settings,
|
|
SyncData,
|
|
SyncDataKey,
|
|
} from '~/types';
|
|
import { pauseRecording, resumeRecording } from '~/utils/recording';
|
|
|
|
const channel = new Channel();
|
|
|
|
void (async () => {
|
|
// assign default value to settings of this extension
|
|
const result =
|
|
((await Browser.storage.sync.get(SyncDataKey.settings)) as SyncData) ||
|
|
undefined;
|
|
const defaultSettings: Settings = {};
|
|
let settings = defaultSettings;
|
|
if (result && result.settings) {
|
|
setDefaultSettings(result.settings, defaultSettings);
|
|
settings = result.settings;
|
|
}
|
|
await Browser.storage.sync.set({
|
|
settings,
|
|
} as SyncData);
|
|
|
|
// When tab is changed during the recording process, pause recording in the old tab and start a new one in the new tab.
|
|
Browser.tabs.onActivated.addListener((activeInfo) => {
|
|
Browser.storage.local
|
|
.get(LocalDataKey.recorderStatus)
|
|
.then(async (data) => {
|
|
const localData = data as LocalData;
|
|
if (!localData || !localData[LocalDataKey.recorderStatus]) return;
|
|
let statusData = localData[LocalDataKey.recorderStatus];
|
|
let { status } = statusData;
|
|
let bufferedEvents: eventWithTime[] | undefined;
|
|
|
|
if (status === RecorderStatus.RECORDING) {
|
|
const result = await pauseRecording(
|
|
channel,
|
|
RecorderStatus.PausedSwitch,
|
|
statusData,
|
|
).catch(async () => {
|
|
/**
|
|
* This error happen when the old tab is closed.
|
|
* In this case, the recording process would be stopped through Browser.tabs.onRemoved API.
|
|
* So we just read the new status here.
|
|
*/
|
|
const localData = (await Browser.storage.local.get(
|
|
LocalDataKey.recorderStatus,
|
|
)) as LocalData;
|
|
return {
|
|
status: localData[LocalDataKey.recorderStatus],
|
|
bufferedEvents,
|
|
};
|
|
});
|
|
if (!result) return;
|
|
statusData = result.status;
|
|
status = statusData.status;
|
|
bufferedEvents = result.bufferedEvents;
|
|
}
|
|
if (status === RecorderStatus.PausedSwitch)
|
|
await resumeRecording(
|
|
channel,
|
|
activeInfo.tabId,
|
|
statusData,
|
|
bufferedEvents,
|
|
);
|
|
})
|
|
.catch(() => {
|
|
// the extension can't access to the tab
|
|
});
|
|
});
|
|
|
|
// If the recording can't start on an invalid tab, resume it when the tab content is updated.
|
|
Browser.tabs.onUpdated.addListener(function (tabId, info) {
|
|
if (info.status !== 'complete') return;
|
|
Browser.storage.local
|
|
.get(LocalDataKey.recorderStatus)
|
|
.then(async (data) => {
|
|
const localData = data as LocalData;
|
|
if (!localData || !localData[LocalDataKey.recorderStatus]) return;
|
|
const { status, activeTabId } = localData[LocalDataKey.recorderStatus];
|
|
if (status !== RecorderStatus.PausedSwitch || activeTabId === tabId)
|
|
return;
|
|
await resumeRecording(
|
|
channel,
|
|
tabId,
|
|
localData[LocalDataKey.recorderStatus],
|
|
);
|
|
})
|
|
.catch(() => {
|
|
// the extension can't access to the tab
|
|
});
|
|
});
|
|
|
|
/**
|
|
* When the current tab is closed, the recording events will be lost because this event is fired after it is closed.
|
|
* This event listener is just used to make sure the recording status is updated.
|
|
*/
|
|
Browser.tabs.onRemoved.addListener((tabId) => {
|
|
Browser.storage.local
|
|
.get(LocalDataKey.recorderStatus)
|
|
.then(async (data) => {
|
|
const localData = data as LocalData;
|
|
if (!localData || !localData[LocalDataKey.recorderStatus]) return;
|
|
const { status, activeTabId, startTimestamp } =
|
|
localData[LocalDataKey.recorderStatus];
|
|
if (activeTabId !== tabId || status !== RecorderStatus.RECORDING)
|
|
return;
|
|
|
|
// Update the recording status to make it resumable after users switch to other tabs.
|
|
const statusData: LocalData[LocalDataKey.recorderStatus] = {
|
|
status: RecorderStatus.PausedSwitch,
|
|
activeTabId,
|
|
startTimestamp,
|
|
pausedTimestamp: Date.now(),
|
|
};
|
|
await Browser.storage.local.set({
|
|
[LocalDataKey.recorderStatus]: statusData,
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
});
|
|
});
|
|
})();
|
|
|
|
/**
|
|
* Update existed settings with new settings.
|
|
* Set new setting values if these properties don't exist in older versions.
|
|
*/
|
|
function setDefaultSettings(
|
|
existedSettings: Record<string, unknown>,
|
|
newSettings: Record<string, unknown>,
|
|
) {
|
|
for (const i in newSettings) {
|
|
// settings[i] contains key-value settings
|
|
if (
|
|
typeof newSettings[i] === 'object' &&
|
|
!(newSettings[i] instanceof Array) &&
|
|
Object.keys(newSettings[i] as Record<string, unknown>).length > 0
|
|
) {
|
|
if (existedSettings[i]) {
|
|
setDefaultSettings(
|
|
existedSettings[i] as Record<string, unknown>,
|
|
newSettings[i] as Record<string, unknown>,
|
|
);
|
|
} else {
|
|
// settings[i] contains several setting items but these have not been set before
|
|
existedSettings[i] = newSettings[i];
|
|
}
|
|
} else if (existedSettings[i] === undefined) {
|
|
// settings[i] is a single setting item and it has not been set before
|
|
existedSettings[i] = newSettings[i];
|
|
}
|
|
}
|
|
}
|