Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b70254c
feat: add rrweb web-extension package
YunFeng0817 Oct 16, 2022
be295bd
refactor: make the extension suitable for manifest v3
YunFeng0817 Oct 17, 2022
6d7ae5d
update tsconfig.json
YunFeng0817 Oct 31, 2022
c99b049
use version_name rather than recorder_version in manifest.json
YunFeng0817 Oct 31, 2022
e87d539
update manifest.json
YunFeng0817 Nov 1, 2022
579d5b8
enable to keep recording after changing tabs
YunFeng0817 Nov 1, 2022
8739a6f
enable to record between tabs and urls
YunFeng0817 Nov 2, 2022
6fd4f2a
fix CI error
YunFeng0817 Nov 2, 2022
7fbc406
Merge branch 'master' into extension
YunFeng0817 Nov 2, 2022
3209471
try to fix CI error
YunFeng0817 Nov 2, 2022
f134ddc
feat: add pause and resume buttons
YunFeng0817 Nov 3, 2022
ab0b588
feat: add a link to new session after recording
YunFeng0817 Nov 3, 2022
0d633e5
improve session list
YunFeng0817 Nov 5, 2022
8246457
refactor: migrate session storage from chrome local storage to indexedDB
YunFeng0817 Nov 5, 2022
5fe51f1
feat: add pagination to session list
YunFeng0817 Nov 5, 2022
1266dae
fix: multiple recorders are started after pausing and resuming process
YunFeng0817 Nov 5, 2022
6186f6a
fix: can't stop recording on firefox browser
YunFeng0817 Nov 5, 2022
0c7e5ef
Merge remote-tracking branch 'origin/master' into extension
YunFeng0817 Nov 5, 2022
5340919
update type import of 'eventWithTime'
YunFeng0817 Nov 5, 2022
9670fd3
fix CI error
YunFeng0817 Nov 5, 2022
196082b
doc: add readme
YunFeng0817 Nov 5, 2022
558ec5d
Merge branch 'master' into extension
YunFeng0817 Nov 7, 2022
c0126c9
Apply suggestions from Justin's code review
YunFeng0817 Nov 12, 2022
2d35e10
refactor: make use of webNavigation API to implement recording consis…
YunFeng0817 Nov 12, 2022
2fbd399
fix firefox compatibility issue and add title to pages
YunFeng0817 Nov 12, 2022
4a56727
add mouseleave listener to enhance the recording liability
YunFeng0817 Nov 13, 2022
6abfb40
fix firefox compatibility issue and improve the experience of recordi…
YunFeng0817 Nov 13, 2022
c931c51
Merge branch 'master' into extension
YunFeng0817 Nov 13, 2022
da99bc2
update tsconfig
YunFeng0817 Nov 13, 2022
d65135c
upgrade vite-plugin-web-extension config to fix some bugs on facebook…
YunFeng0817 Nov 17, 2022
f08f334
update import links
YunFeng0817 Nov 17, 2022
46140be
refactor: cross tab recording mechanism
YunFeng0817 Nov 17, 2022
6665ca4
Merge branch 'master' into extension
YunFeng0817 Nov 18, 2022
36248e2
refactor: slipt util/index.ts into multiple files
YunFeng0817 Dec 1, 2022
d978ab6
implement cross-origin iframe recording
YunFeng0817 Dec 2, 2022
70e6155
fix: regression of issue: ShadowHost can't be a string (issue 941)
YunFeng0817 Jan 13, 2023
407a48a
refactor shadow dom recording to make tests cover key code
YunFeng0817 Jan 13, 2023
719ca76
Merge branch 'master' into extension
YunFeng0817 Jan 13, 2023
2409f76
Merge branch 'fix-shadow-dom-bug' into extension
YunFeng0817 Jan 17, 2023
1174194
Merge branch 'master' into extension
YunFeng0817 Feb 11, 2023
4e779f9
Apply formatting changes
Feb 11, 2023
36a8c00
increase the node memory limitation to avoid CI failure
YunFeng0817 Feb 11, 2023
6101714
Merge remote-tracking branch 'origin/master' into extension
YunFeng0817 Feb 12, 2023
1041fac
Create lovely-pears-cross.md
Juice10 Feb 13, 2023
1e4c582
Apply formatting changes
Juice10 Feb 13, 2023
86a88e0
Update packages/web-extension/package.json
Juice10 Feb 13, 2023
1f8a29a
Update .changeset/lovely-pears-cross.md
Juice10 Feb 13, 2023
3e5ab6f
Merge branch 'master' into extension
YunFeng0817 Feb 13, 2023
802b9b2
update change logs
YunFeng0817 Feb 13, 2023
832d7eb
delete duplicated property
YunFeng0817 Feb 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: make the extension suitable for manifest v3
  • Loading branch information
YunFeng0817 committed Oct 17, 2022
commit be295bdddb5bac34af87b3ebc09be38cb8282bee
14 changes: 8 additions & 6 deletions packages/web-extension/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{
"name": "web-extension",
"version": "0.2.0",
"version": "2.0.0",
"description": "The web extension of rrweb which helps to run rrweb on any website out of box",
"author": "Yanzhen Yu, Yun Feng",
"license": "MIT",
"private": true,
"scripts": {
"dev:chrome": "cross-env TARGET_BROWSER=chrome vite dev",
"dev:firefox": "cross-env TARGET_BROWSER=firefox vite dev",
Expand All @@ -20,12 +22,10 @@
"@vitejs/plugin-react": "^2.1.0",
"cross-env": "^7.0.3",
"jest": "^29.0.3",
"rrweb": "^2.0.0-alpha.3",
"rrweb-player": "^1.0.0-alpha.3",
"ts-jest": "^29.0.1",
"type-fest": "^2.19.0",
"typescript": "^4.8.3",
"vite": "^3.1.2",
"typescript": "^4.7.3",
"vite": "^3.1.8",
"vite-plugin-web-extension": "^1.4.4",
"vite-plugin-zip": "^1.0.1",
"webextension-polyfill": "^0.10.0"
Expand All @@ -40,6 +40,8 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.4.0",
"react-router-dom": "^6.4.1"
"react-router-dom": "^6.4.1",
"rrweb": "^2.0.0-alpha.3",
"rrweb-player": "^1.0.0-alpha.3"
}
}
39 changes: 1 addition & 38 deletions packages/web-extension/src/background/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import Browser from 'webextension-polyfill';
import { Settings, SyncData, SyncDataKey } from '../types';
import { fetchPackageVersions, getPlayerURL, getRecorderURL } from '../utils';

void (async () => {
const recorderVersions = await fetchPackageVersions('rrweb');
const playerVersions = await fetchPackageVersions('rrweb-player');
// Use the latest version.
const defaultRecorderVersion = recorderVersions[0];
const defaultPlayerVersion = playerVersions[0];
// assign default value to settings of this extension
const result =
((await Browser.storage.sync.get(SyncDataKey.settings)) as SyncData) ||
undefined;
const defaultSettings: Settings = {
recorderURL: getRecorderURL(defaultRecorderVersion),
recorderVersion: defaultRecorderVersion,
playerURL: getPlayerURL(defaultPlayerVersion),
playerVersion: defaultPlayerVersion,
};
const defaultSettings: Settings = {};
let settings = defaultSettings;
if (result && result.settings) {
setDefaultSettings(result.settings, defaultSettings);
Expand All @@ -26,16 +15,8 @@ void (async () => {
await Browser.storage.sync.set({
settings,
} as SyncData);
void fetchSourceCode(settings);
})();

Browser.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && changes.settings) {
const newValue = changes.settings.newValue as Settings;
void fetchSourceCode(newValue);
}
});

/**
* Update existed settings with new settings.
* Set new setting values if these properties don't exist in older versions.
Expand Down Expand Up @@ -66,21 +47,3 @@ function setDefaultSettings(
}
}
}

/**
* Fetch rrweb source code from recorder URL and player URL.
*/
async function fetchSourceCode(settings: Settings) {
if (settings.recorderURL) {
const code = await (await fetch(settings.recorderURL)).text();
await Browser.storage.local.set({
recorder_code: code,
});
}
if (settings.playerURL) {
const code = await (await fetch(settings.playerURL)).text();
await Browser.storage.local.set({
player_code: code,
});
}
}
167 changes: 59 additions & 108 deletions packages/web-extension/src/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,151 +7,107 @@ import {
RecorderStatus,
ServiceName,
Session,
StartRecordResponse,
StopRecordResponse,
SyncData,
SyncDataKey,
RecordStartedMessage,
RecordStoppedMessage,
HeartBreathMessage,
MessageName,
} from '../types';
import Channel from '../utils/channel';

const channel = new Channel();

void (async () => {
let storedEvents: eventWithTime[] = [];
let startResponseCb: ((response: StartRecordResponse) => void) | undefined =
undefined;
let startResponseCb:
| ((response: RecordStartedMessage) => void)
| undefined = undefined;
// The callback function to remove the recorder from the page.
let clearRecorderCb: (() => void) | undefined = undefined;
channel.provide(ServiceName.StartRecord, async () => {
clearRecorderCb = await startRecord();
window.postMessage({ message: 'start-record' });
clearRecorderCb = startRecord();
return new Promise((resolve) => {
startResponseCb = (response) => {
resolve(response);
};
});
});
let stopResponseCb: ((response: StopRecordResponse) => void) | undefined =
undefined;
let stopResponseCb:
| ((response: RecordStoppedMessage) => void)
| undefined = undefined;
channel.provide(ServiceName.StopRecord, () => {
window.postMessage({ message: 'stop-record' });
window.postMessage({ message: MessageName.StopRecord });
return new Promise((resolve) => {
stopResponseCb = (response: StopRecordResponse) => {
stopResponseCb = (response: RecordStoppedMessage) => {
resolve(response);
};
});
});

window.addEventListener('message', (event) => {
const data = event.data as
| StartRecordResponse
| StopRecordResponse
| HeartBreathMessage;
if (data.message === 'start-record-response' && startResponseCb)
startResponseCb(data);
else if (data.message === 'stop-record-response' && stopResponseCb) {
stopResponseCb(data);
void saveEvents(storedEvents.concat(data.events));
clearRecorderCb?.();
clearRecorderCb = undefined;
storedEvents = [];
} else if (data.message === 'heart-beat') {
const events = data.events;
void Browser.storage.local.set({
[LocalDataKey.bufferedEvents]: storedEvents.concat(events),
});
}
});
window.addEventListener(
'message',
(event: {
data:
| RecordStartedMessage
| RecordStoppedMessage
| HeartBreathMessage
| {
message: MessageName;
};
}) => {
if (event.data.message === MessageName.RecordScriptReady)
window.postMessage({ message: MessageName.StartRecord });
else if (
event.data.message === MessageName.RecordStarted &&
startResponseCb
)
startResponseCb(event.data as RecordStartedMessage);
else if (
event.data.message === MessageName.RecordStopped &&
stopResponseCb
) {
const data = event.data as RecordStoppedMessage;
stopResponseCb(data);
void saveEvents(storedEvents.concat(data.events));
clearRecorderCb?.();
clearRecorderCb = undefined;
storedEvents = [];
} else if (event.data.message === MessageName.HeartBeat) {
void Browser.storage.local.set({
[LocalDataKey.bufferedEvents]: storedEvents.concat(
(event.data as HeartBreathMessage).events,
),
});
}
},
);

const localData = (await Browser.storage.local.get()) as LocalData;
if (localData?.recorder_status?.status === RecorderStatus.RECORDING) {
clearRecorderCb = await startRecord();
clearRecorderCb = startRecord();
storedEvents = localData.buffered_events || [];
}
})();

async function startRecord() {
const data = await Browser.storage.local.get('recorder_code');
const recorderCode = data['recorder_code'] as string | undefined;
if (!recorderCode || recorderCode.length === 0) return;

function startRecord() {
const scriptEl = document.createElement('script');
try {
const uniqueVariablePrefix = '__rrweb_extension_unique_prefix_';
const events = `${uniqueVariablePrefix}events`;
const stopFn = `${uniqueVariablePrefix}stopFn`;
const setIntervalId = `${uniqueVariablePrefix}setIntervalId`;
const record = `${uniqueVariablePrefix}record`;
scriptEl.textContent = `
${recorderCode}
var ${events} = [];
var ${stopFn} = null;
var ${setIntervalId} = null;

function ${record}() {
${events} = [];
let recorder;
try {
recorder = rrwebRecord;
} catch (e) {
recorder = rrweb.record;
}
${stopFn} = recorder({
emit: (event) => {
${events}.push(event);
},
});
}

window.addEventListener('message', (event) => {
const data = event.data;
if (data.message === 'stop-record') {
if (${stopFn}) ${stopFn}();
clearInterval(${setIntervalId});
window.postMessage({
message: 'stop-record-response',
events: ${events},
endTimestamp: Date.now(),
});
}
});

window.postMessage({
message: 'start-record-response',
startTimestamp: Date.now(),
});
${record}();

${setIntervalId} = setInterval(() => {
window.postMessage({
message: 'heart-beat',
events: ${events},
});
}, 50);
`;
document.documentElement.appendChild(scriptEl);
return () => {
document.documentElement.removeChild(scriptEl);
};
} catch (e) {
scriptEl.src = Browser.runtime.getURL('content/inject.js');
document.documentElement.appendChild(scriptEl);
return () => {
document.documentElement.removeChild(scriptEl);
}
};
}

async function saveEvents(events: eventWithTime[]) {
const recorderSettings = (await Browser.storage.sync.get(
SyncDataKey.settings,
)) as SyncData;
const { recorderVersion, recorderURL } = recorderSettings.settings;
const newSession: Session = {
id: nanoid(),
name: document.title,
tags: [],
events,
createTimestamp: Date.now(),
modifyTimestamp: Date.now(),
recorderVersion,
recorderURL,
recorderVersion: ((Browser.runtime.getManifest() as unknown) as {
recorder_version: string;
}).recorder_version,
};
const data = (await Browser.storage.local.get(
LocalDataKey.sessions,
Expand All @@ -160,8 +116,3 @@ async function saveEvents(events: eventWithTime[]) {
data.sessions[newSession.id] = newSession;
await Browser.storage.local.set(data);
}

type HeartBreathMessage = {
message: 'heart-beat';
events: eventWithTime[];
};
59 changes: 59 additions & 0 deletions packages/web-extension/src/content/inject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { record } from 'rrweb';
import { eventWithTime, recordOptions } from 'rrweb/typings/types';
import { MessageName, RecordStartedMessage } from '../types';

const events: eventWithTime[] = [];
let stopFn: (() => void) | null = null;
let setIntervalId: number | null = null;

function startRecord(config: recordOptions<eventWithTime>) {
events.length = 0;
stopFn =
record({
emit: (event) => {
events.push(event);
},
...config,
}) || null;
window.postMessage({
message: MessageName.RecordStarted,
startTimestamp: Date.now(),
} as RecordStartedMessage);
setIntervalId = (setInterval(() => {
window.postMessage({
message: MessageName.HeartBeat,
events,
});
}, 50) as unknown) as number;
}

window.addEventListener(
'message',
(event: {
data: {
message: MessageName;
config?: recordOptions<eventWithTime>;
};
}) => {
const data = event.data;
const eventHandler = {
[MessageName.StartRecord]: () => {
startRecord(data.config || {});
},
[MessageName.StopRecord]: () => {
if (stopFn) stopFn();
if (setIntervalId) clearInterval(setIntervalId);
window.postMessage({
message: MessageName.RecordStopped,
events,
endTimestamp: Date.now(),
});
},
} as Record<MessageName, () => void>;
if (eventHandler[data.message]) eventHandler[data.message]();
},
);

window.postMessage({
message: MessageName.RecordScriptReady,
});
Loading