Skip to content

Commit

Permalink
rrweb extension implementation (#1044)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
YunFeng0817 and Juice10 authored Feb 13, 2023
1 parent 227d43a commit 282c8fa
Show file tree
Hide file tree
Showing 40 changed files with 5,950 additions and 508 deletions.
2 changes: 0 additions & 2 deletions .changeset/beige-numbers-enjoy.md

This file was deleted.

5 changes: 5 additions & 0 deletions .changeset/lovely-pears-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rrweb/web-extension': patch
---

Add rrweb browser extension
2 changes: 1 addition & 1 deletion .changeset/nervous-poets-grin.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
'rrweb-snapshot': patch
---

- [`fe69bd6`](https://github.com/rrweb-io/rrweb/commit/fe69bd6456cead304bfc77cf72c9db0f8c030842) [#1087](https://github.com/rrweb-io/rrweb/pull/1087) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - Refactor all suffix of bundled scripts with commonjs module from 'js' to cjs.
Refactor all suffix of bundled scripts with commonjs module from 'js' to cjs [#1087](https://github.com/rrweb-io/rrweb/pull/1087).
2 changes: 0 additions & 2 deletions .changeset/olive-worms-pump.md

This file was deleted.

2 changes: 1 addition & 1 deletion .changeset/real-trains-switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
'rrweb': patch
---

- [`4ee86fe`](https://github.com/rrweb-io/rrweb/commit/4ee86fe66d3e1fe7071f9c8764d82a6fa5c71d57) [#1091](https://github.com/rrweb-io/rrweb/pull/1091) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - Fix: improve rrdom robustness.
Fix: improve rrdom robustness [#1091](https://github.com/rrweb-io/rrweb/pull/1091).
2 changes: 1 addition & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
run: yarn

- name: Build Project
run: yarn build:all
run: NODE_OPTIONS='--max-old-space-size=4096' yarn build:all

- name: Check types
run: yarn turbo run check-types
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
id: changesets
uses: changesets/action@v1
with:
publish: yarn run release
publish: NODE_OPTIONS='--max-old-space-size=4096' yarn run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/style-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Install Dependencies
run: yarn
- name: Build Packages
run: yarn build:all
run: NODE_OPTIONS='--max-old-space-size=4096' yarn build:all
- name: Eslint Check
run: yarn turbo run lint
- name: Save Code Linting Report JSON
Expand Down
7 changes: 4 additions & 3 deletions .vscode/rrweb-monorepo.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
"path": "../packages/rrweb-snapshot"
},
{
"name": "@rrweb/types",
"path": "../packages/types"
}
"name": "web-extension (package)",
"path": "../packages/web-extension"
},
{ "name": "@rrweb/types", "path": "../packages/types" }
],
"settings": {
"jest.disabledWorkspaceFolders": [
Expand Down
32 changes: 32 additions & 0 deletions packages/web-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<p align="center">
<img width="100px" height="100px" src="https://www.rrweb.io/favicon.png">
</p>

# rrweb extension

The package web-extension provides a browser extension for recording and replaying web pages.

## Installation

```
yarn install
```

## Build

```bash
# build for chrome
yarn build:chrome

# build for firefox
yarn build:firefox
```

## Development

```bash
# start a development chrome browser
yarn dev:chrome
# start a development firefox browser
yarn dev:firefox
```
47 changes: 47 additions & 0 deletions packages/web-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@rrweb/web-extension",
"private": true,
"version": "2.0.0",
"description": "The web extension of rrweb which helps to run rrweb on any website out of box",
"author": "rrweb-io",
"license": "MIT",
"scripts": {
"dev:chrome": "cross-env TARGET_BROWSER=chrome vite dev",
"dev:firefox": "cross-env TARGET_BROWSER=firefox vite dev",
"build:chrome": "cross-env TARGET_BROWSER=chrome vite build",
"build:firefox": "cross-env TARGET_BROWSER=firefox vite build",
"pack:chrome": "cross-env TARGET_BROWSER=chrome ZIP=true vite build",
"pack:firefox": "cross-env TARGET_BROWSER=firefox ZIP=true vite build",
"check-types": "tsc -noEmit",
"prepublish": "npm run pack:chrome && npm run pack:firefox"
},
"devDependencies": {
"@rrweb/types": "^2.0.0-alpha.4",
"@types/react-dom": "^18.0.6",
"@types/webextension-polyfill": "^0.9.1",
"@vitejs/plugin-react": "^2.1.0",
"cross-env": "^7.0.3",
"type-fest": "^2.19.0",
"typescript": "^4.7.3",
"vite": "^3.1.8",
"vite-plugin-web-extension": "^1.4.5",
"vite-plugin-zip": "^1.0.1",
"webextension-polyfill": "^0.10.0"
},
"dependencies": {
"@chakra-ui/react": "^2.3.4",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@tanstack/react-table": "^8.5.22",
"framer-motion": "^7.3.6",
"idb": "^7.1.1",
"mitt": "^3.0.0",
"nanoid": "^4.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.4.0",
"react-router-dom": "^6.4.1",
"rrweb": "^2.0.0-alpha.4",
"rrweb-player": "^1.0.0-alpha.4"
}
}
Binary file added packages/web-extension/src/assets/icon128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/web-extension/src/assets/icon16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/web-extension/src/assets/icon48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
162 changes: 162 additions & 0 deletions packages/web-extension/src/background/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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];
}
}
}
33 changes: 33 additions & 0 deletions packages/web-extension/src/components/CircleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Button, ButtonProps } from '@chakra-ui/react';

interface CircleButtonProps extends ButtonProps {
diameter: number;
onClick?: () => void;
children?: React.ReactNode;
title?: string;
}

export function CircleButton({
diameter,
onClick,
children,
title,
...rest
}: CircleButtonProps) {
return (
<Button
w={`${diameter}rem`}
h={`${diameter}rem`}
padding={`${diameter / 2}rem`}
borderRadius={9999}
textAlign="center"
bgColor="gray.100"
boxSizing="content-box"
onClick={onClick}
title={title}
{...rest}
>
{children}
</Button>
);
}
Loading

0 comments on commit 282c8fa

Please sign in to comment.