* Add `recordCrossOriginIframe` setting * Set up messaging between iframes * should emit full snapshot event from iframe as mutation event * this.mirror was dropped on attachIframe * should use unique id for child of iframe * Cross origin iframe recording in `yarn live-stream` * Root iframe check thats supported by firefox * Live stream: Inject script in all frames * Record same origin and cross origin iframes differently * Should map Input events correctly * Turn on other tests * Fix compatibility with newer puppeteer * puppeteer vs 12 seems stable without to many changes needed * normalize port numbers in snapshots * Handle scroll and ViewportResize events in cross origin iframe * Correctly map cross origin mutations * Map selection events for cross origin iframes * Map canvas mutations for cross origin iframes * Update snapshot to include canvas events * Skip all meta events * Support custom events as best we can in cross origin iframes * Use earliest version of puppeteer that works with cross origin live-stream * Map mouse/touch interaction events * Update snapshots for correctly mapped click events * Tweak tests for new puppeteer version * Map MediaInteraction correctly for cross origin iframes * Make tests consistent between high and low dpi devices * Make test less flaky * Make test less flaky * Make test less flaky * Make test less flaky * Add support for styles in cross origin iframes * Map traditional stylesheet mutations on cross origin iframes * Add todo * Add iframe mirror * Get iframe manager to use iframe mirrors internally * Rename `IframeMirror` to `CrossOriginIframeMirror` * Setup basic cross origin canvas webrtc streaming * Clean up removed canvas elements * reset style mirror on new full snapshot * Fix cross origin canvas webrtc streaming * Make emit optional * Run tests on github actions * Upload image artifacts from failed tests * Use newer github actions * Test: hopefully adding more wait will fix it * add extra wait * Fix image snapshot tests * Make tests run with new puppeteer version * upgrade eslint-plugin-jest * Chore: Remove travis ci as ci's running on github actions * Chore: Support recording cross origin iframe in repl * Force developers to update the cross origin iframe mapping when adding new events https://github.com/rrweb-io/rrweb/pull/1035#discussion_r1012516277 * Document cross origin iframe recording * Docs: cross origin iframes recording methods * Docs: AI translated, cross origin iframe recording * rename getParentId to getId * Migrate to @rrweb/types * Run on pull request * doc: improve Chinese doc * Rename `parentId` to `Id` Co-authored-by: Mark-Fenng <f18846188605@gmail.com>
271 lines
7.3 KiB
JavaScript
271 lines
7.3 KiB
JavaScript
/* eslint:disable: no-console */
|
|
|
|
import * as path from 'path';
|
|
import * as fs from 'fs';
|
|
import { EventEmitter } from 'node:events';
|
|
import inquirer from 'inquirer';
|
|
import puppeteer from 'puppeteer';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
import { startServer, getServerURL } from './utils.js';
|
|
|
|
// Turn on devtools for debugging:
|
|
const devtools = false;
|
|
const defaultURL = 'https://webglsamples.org/';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const emitter = new EventEmitter();
|
|
const code = fs.readFileSync(path.join(__dirname, '../dist/rrweb.js'), 'utf8');
|
|
const pluginCode = fs.readFileSync(
|
|
path.join(__dirname, '../dist/plugins/canvas-webrtc-record.js'),
|
|
'utf8',
|
|
);
|
|
|
|
async function injectRecording(frame) {
|
|
await frame.evaluate(
|
|
(rrwebCode, pluginCode) => {
|
|
const win = window;
|
|
if (win.__IS_RECORDING__) return;
|
|
win.__IS_RECORDING__ = true;
|
|
|
|
(async () => {
|
|
function loadScript(code) {
|
|
const s = document.createElement('script');
|
|
s.type = 'text/javascript';
|
|
s.innerHTML = code;
|
|
if (document.head) {
|
|
document.head.append(s);
|
|
} else {
|
|
requestAnimationFrame(() => {
|
|
document.head.append(s);
|
|
});
|
|
}
|
|
}
|
|
loadScript(rrwebCode);
|
|
loadScript(pluginCode);
|
|
|
|
win.events = [];
|
|
window.record = win.rrweb.record;
|
|
window.plugin = new rrwebCanvasWebRTCRecord.RRWebPluginCanvasWebRTCRecord(
|
|
{
|
|
signalSendCallback: (msg) => {
|
|
// [record#callback] provides canvas id, stream, and webrtc sdpOffer signal & connect message
|
|
_signal(msg);
|
|
},
|
|
},
|
|
);
|
|
|
|
window.record({
|
|
emit: (event) => {
|
|
win.events.push(event);
|
|
win._captureEvent(event);
|
|
},
|
|
plugins: [window.plugin.initPlugin()],
|
|
recordCanvas: false,
|
|
recordCrossOriginIframes: true,
|
|
collectFonts: true,
|
|
inlineImages: true,
|
|
});
|
|
})();
|
|
},
|
|
code,
|
|
pluginCode,
|
|
);
|
|
}
|
|
|
|
async function startReplay(page, serverURL, recordedPage) {
|
|
await recordedPage.exposeFunction('_signal', async (signal) => {
|
|
await page.evaluate((signal) => {
|
|
// [replay#signalReceive] setups up peer and starts creating counter offer
|
|
window.plugin.signalReceive(signal);
|
|
}, signal);
|
|
});
|
|
await page.exposeFunction('_signal', async (signal) => {
|
|
await recordedPage.evaluate((signal) => {
|
|
// [record#signalReceive] setups up webrtc connection
|
|
window.plugin.signalReceive(signal);
|
|
}, signal);
|
|
});
|
|
await page.exposeFunction('_canvas', async (id) => {
|
|
await recordedPage.evaluate((id) => {
|
|
// [record#setupStream] sets up the canvas stream for a given id.
|
|
const stream = window.plugin.setupStream(id);
|
|
console.log('stream for', id, '=>', stream);
|
|
}, id);
|
|
});
|
|
|
|
await page.addScriptTag({ url: `${serverURL}/rrweb.js` });
|
|
await page.addScriptTag({
|
|
url: `${serverURL}/plugins/canvas-webrtc-replay.js`,
|
|
});
|
|
|
|
return page.evaluate(() => {
|
|
window.plugin = new rrwebCanvasWebRTCReplay.RRWebPluginCanvasWebRTCReplay({
|
|
canvasFoundCallback(canvas, context) {
|
|
console.log('canvas', canvas, context);
|
|
// [replay#onBuild] gets id of canvas element and sends to recorded page
|
|
_canvas(context.id);
|
|
},
|
|
signalSendCallback(data) {
|
|
_signal(JSON.stringify(data));
|
|
},
|
|
});
|
|
|
|
window.replayer = new rrweb.Replayer([], {
|
|
UNSAFE_replayCanvas: true,
|
|
liveMode: true,
|
|
plugins: [window.plugin.initPlugin()],
|
|
});
|
|
window.replayer.startLive();
|
|
const style = new CSSStyleSheet();
|
|
style.replaceSync('body {margin: 0;} iframe {border: none;}');
|
|
document.adoptedStyleSheets = [style];
|
|
});
|
|
}
|
|
|
|
async function resizeWindow(page, top, left, width, height) {
|
|
const session = await page.target().createCDPSession();
|
|
await page.setViewport({ height, width });
|
|
const { windowId } = await session.send('Browser.getWindowForTarget');
|
|
await session.send('Browser.setWindowBounds', {
|
|
bounds: { top, left, height, width },
|
|
windowId,
|
|
});
|
|
}
|
|
|
|
void (async () => {
|
|
let server;
|
|
let serverURL;
|
|
|
|
await start();
|
|
|
|
async function start() {
|
|
server = await startServer();
|
|
serverURL = getServerURL(server);
|
|
|
|
const { url } = await inquirer.prompt([
|
|
{
|
|
type: 'input',
|
|
name: 'url',
|
|
message: `Enter the url you want to record, e.g ${defaultURL}: `,
|
|
},
|
|
]);
|
|
|
|
await record(url);
|
|
|
|
const { shouldRecordAnother } = await inquirer.prompt([
|
|
{
|
|
type: 'confirm',
|
|
name: 'shouldRecordAnother',
|
|
message: 'Record another one?',
|
|
},
|
|
]);
|
|
|
|
emitter.emit('done');
|
|
|
|
if (shouldRecordAnother) {
|
|
await start();
|
|
} else {
|
|
process.exit();
|
|
}
|
|
}
|
|
|
|
async function record(url) {
|
|
if (url === '') url = defaultURL;
|
|
|
|
const replayingBrowser = await puppeteer.launch({
|
|
headless: false,
|
|
devtools,
|
|
defaultViewport: {
|
|
width: 1600,
|
|
height: 900,
|
|
},
|
|
args: [
|
|
'--start-maximized',
|
|
'--ignore-certificate-errors',
|
|
'--no-sandbox',
|
|
],
|
|
});
|
|
|
|
const replayerPage = (await replayingBrowser.pages())[0];
|
|
await replayerPage.goto('about:blank');
|
|
|
|
await replayerPage.addStyleTag({
|
|
path: path.resolve(__dirname, '../dist/rrweb.css'),
|
|
});
|
|
|
|
const recordingBrowser = await puppeteer.launch({
|
|
headless: false,
|
|
devtools,
|
|
defaultViewport: {
|
|
width: 1600,
|
|
height: 900,
|
|
},
|
|
args: [
|
|
'--start-maximized',
|
|
'--ignore-certificate-errors',
|
|
'--no-sandbox',
|
|
],
|
|
});
|
|
|
|
const recordedPage = (await recordingBrowser.pages())[0];
|
|
if (!recordedPage) {
|
|
throw new Error('No recorded page found');
|
|
}
|
|
// disables content security policy which enables us to insert rrweb as a script tag
|
|
await recordedPage.setBypassCSP(true);
|
|
|
|
replayerPage.on('console', (msg) =>
|
|
console.log('REPLAY PAGE LOG:', msg.text()),
|
|
);
|
|
recordedPage.on('console', (msg) =>
|
|
console.log('RECORD PAGE LOG:', msg.text()),
|
|
);
|
|
await startReplay(replayerPage, serverURL, recordedPage);
|
|
|
|
await Promise.all([
|
|
resizeWindow(recordedPage, 0, 0, 800, 800),
|
|
resizeWindow(replayerPage, 0, 800, 800, 800),
|
|
]);
|
|
|
|
await recordedPage.exposeFunction('_captureEvent', (event) => {
|
|
replayerPage.evaluate((event) => {
|
|
window.replayer.addEvent(event);
|
|
}, event);
|
|
});
|
|
|
|
recordedPage.on('framenavigated', async (frame) => {
|
|
console.log('framenavigated');
|
|
await injectRecording(frame, serverURL);
|
|
});
|
|
|
|
await recordedPage.goto(url, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 300000,
|
|
});
|
|
|
|
if (!replayerPage) throw new Error('No replayer page found');
|
|
|
|
emitter.once('done', async () => {
|
|
const pages = [
|
|
...(await recordingBrowser.pages()),
|
|
...(await replayingBrowser.pages()),
|
|
];
|
|
await server.close();
|
|
await Promise.all(pages.map((page) => page.close()));
|
|
await recordingBrowser.close();
|
|
await replayingBrowser.close();
|
|
});
|
|
}
|
|
|
|
process
|
|
.on('uncaughtException', (error) => {
|
|
console.error(error);
|
|
})
|
|
.on('unhandledRejection', (error) => {
|
|
console.error(error);
|
|
});
|
|
})();
|