feat(elements): Added sio-recorder element for recording and replaying sessions
This commit is contained in:
@@ -2,3 +2,4 @@ export * from './sio-fab.js';
|
||||
export * from './sio-combox.js';
|
||||
export * from './sio-subwidget-onboardme.js';
|
||||
export * from './sio-subwidget-conversations.js';
|
||||
export * from './sio-recorder.js';
|
||||
|
188
ts_web/elements/sio-recorder.ts
Normal file
188
ts_web/elements/sio-recorder.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import {
|
||||
DeesElement,
|
||||
html,
|
||||
css,
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
} from '@design.estate/dees-element';
|
||||
import * as rrwebMod from 'rrweb';
|
||||
import rrwebPlayerMod from 'rrweb-player';
|
||||
|
||||
const rrweb: any = rrwebMod;
|
||||
const rrwebPlayer: typeof rrwebPlayerMod.default = rrwebPlayerMod as any;
|
||||
|
||||
/**
|
||||
* Use rrweb's eventWithTime if you like strict typing:
|
||||
*
|
||||
* import { eventWithTime } from 'rrweb';
|
||||
* export interface IRecordingEvent extends eventWithTime {}
|
||||
*
|
||||
* Here, for brevity, we define an empty interface
|
||||
* and cast all events to any.
|
||||
*/
|
||||
export interface IRecordingEvent {}
|
||||
|
||||
@customElement('sio-recorder')
|
||||
export class SioRecorder extends DeesElement {
|
||||
public static demo = () => html`<sio-recorder></sio-recorder>`;
|
||||
|
||||
/**
|
||||
* Holds all recorded events from rrweb.
|
||||
*/
|
||||
private events: IRecordingEvent[] = [];
|
||||
|
||||
/**
|
||||
* A reference to rrweb's stop recording function.
|
||||
* We'll store it when we begin a record session so we can call it later.
|
||||
*/
|
||||
private stopFn: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Query for the div in our template that will be used for playback.
|
||||
*/
|
||||
@query('#playback')
|
||||
private playbackDiv!: HTMLDivElement;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
/* The playback container: set a fixed size for demonstration. */
|
||||
#playback {
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div id="playback"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle: Called when the element is inserted into the DOM.
|
||||
*/
|
||||
async connectedCallback(): Promise<void> {
|
||||
super.connectedCallback();
|
||||
console.log('sio-recorder connectedCallback');
|
||||
|
||||
// Start recording immediately
|
||||
this.startRecording();
|
||||
|
||||
// Use the domtools-based approach you have (Ctrl+H):
|
||||
const domtools = await this.domtoolsPromise;
|
||||
domtools.keyboard
|
||||
.on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.H])
|
||||
.subscribe(this.handleKeydown);
|
||||
await this.domtoolsPromise;
|
||||
this.domtools.convenience.smartdelay.delayFor(2000).then(() => {
|
||||
// this.handleKeydown();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle: Called when the element is removed from the DOM.
|
||||
*/
|
||||
public async disconnectedCallback(): Promise<void> {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// Stop recording if it's still running
|
||||
this.stopRecording();
|
||||
|
||||
// Clean up your subscription or any global listeners if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an rrweb recording session that tracks the entire DOM,
|
||||
* including canvases and cross-origin iframes (if permissible).
|
||||
*/
|
||||
private startRecording(): void {
|
||||
this.events = [];
|
||||
|
||||
// For capturing "everything," enable advanced flags:
|
||||
this.stopFn = rrweb.record({
|
||||
emit: (event: any) => {
|
||||
// If you have a stricter type:
|
||||
// this.events.push(event as IRecordingEvent);
|
||||
// else store as any:
|
||||
this.events.push(event);
|
||||
},
|
||||
// Some recommended settings to capture the "complete" page:
|
||||
recordCanvas: true, // record canvas elements
|
||||
recordCrossOriginIframes: true, // attempt capturing cross-origin iframes
|
||||
// checkoutEveryNms: 1000, // check every N milliseconds
|
||||
});
|
||||
|
||||
console.log('Recording has started...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the rrweb recording session
|
||||
*/
|
||||
private stopRecording(): void {
|
||||
if (this.stopFn) {
|
||||
this.stopFn();
|
||||
this.stopFn = null;
|
||||
console.log('Recording has stopped.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays back the recorded events in the `playbackDiv`
|
||||
*/
|
||||
private async playRecording(): Promise<void> {
|
||||
await this.domtoolsPromise;
|
||||
if (!this.playbackDiv) return;
|
||||
const replayer =new rrwebPlayer({
|
||||
target: this.playbackDiv, // customizable root element
|
||||
props: {
|
||||
events: this.events as any,
|
||||
root: this.playbackDiv,
|
||||
showController: false,
|
||||
width: this.playbackDiv.offsetWidth,
|
||||
height: this.playbackDiv.offsetHeight,
|
||||
|
||||
},
|
||||
});
|
||||
this.domtools.convenience.smartdelay.delayFor(0).then(async () => {
|
||||
while (true) {
|
||||
await this.domtools.convenience.smartdelay.delayFor(30000);
|
||||
await replayer.play();
|
||||
await this.domtools.convenience.smartdelay.delayFor(0);
|
||||
// this.fixPosition();
|
||||
}
|
||||
});
|
||||
this.fixPosition();
|
||||
}
|
||||
|
||||
public async fixPosition() {
|
||||
await this.domtoolsPromise;
|
||||
await this.domtools.convenience.smartdelay.delayFor(0);
|
||||
const iframe = this.shadowRoot.querySelector('iframe');
|
||||
const replayerWrapper = this.shadowRoot.querySelector('.replayer-wrapper') as HTMLElement;
|
||||
const replayerMouse = this.shadowRoot.querySelector('.replayer-mouse') as HTMLElement;
|
||||
const replayerMouseTail = this.shadowRoot.querySelector('.replayer-mouse-tail') as HTMLElement;
|
||||
replayerWrapper.style.position = 'absolute';
|
||||
replayerWrapper.style.top = '0px';
|
||||
replayerWrapper.style.left = '0px';
|
||||
replayerWrapper.style.transformOrigin = 'center center';
|
||||
replayerMouse.style.position = 'absolute';
|
||||
replayerMouseTail.style.position = 'absolute';
|
||||
iframe.style.position = 'absolute';
|
||||
iframe.style.top = '0px';
|
||||
iframe.style.left = '0px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Keydown handler. If Ctrl + H is pressed, stop the recording and replay the session
|
||||
*/
|
||||
private handleKeydown = (): void => {
|
||||
this.stopRecording();
|
||||
this.playRecording();
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user