feat(elements): Added sio-recorder element for recording and replaying sessions

This commit is contained in:
Philipp Kunz 2025-01-25 04:21:30 +01:00
parent 39fac4317d
commit 8603b7876f
7 changed files with 284 additions and 5 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## 2025-01-25 - 1.2.0 - feat(elements)
Added sio-recorder element for recording and replaying sessions
- Introduced a new 'sio-recorder' custom element that allows for recording and replaying DOM events.
- Integrated rrweb and rrweb-player dependencies for session recording and playback.
- Updated import/export in ts_web/elements/index.ts to include sio-recorder.
- Fixed assetbroker URL in the html index.html file.
## 2024-12-27 - 1.1.0 - feat(ci) ## 2024-12-27 - 1.1.0 - feat(ci)
Add Gitea workflows for CI/CD process. Add Gitea workflows for CI/CD process.

View File

@ -8,9 +8,9 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!--Lets load standard fonts--> <!--Lets load standard fonts-->
<link rel="preconnect" href="https://" crossorigin> <link rel="preconnect" href="https://assetbroker.lossless.one" crossorigin>
<link rel="stylesheet" href="https:///fonts/fonts.css"> <link rel="stylesheet" href="https://assetbroker.lossless.one/fonts/fonts.css">
<style> <style>
body { body {
margin: 0px; margin: 0px;

View File

@ -20,7 +20,10 @@
"@design.estate/dees-element": "^2.0.39", "@design.estate/dees-element": "^2.0.39",
"@design.estate/dees-wcctools": "^1.0.90", "@design.estate/dees-wcctools": "^1.0.90",
"@losslessone_private/loint-pubapi": "^1.0.14", "@losslessone_private/loint-pubapi": "^1.0.14",
"@social.io/interfaces": "^1.0.5" "@social.io/interfaces": "^1.0.5",
"rrweb": "2.0.0-alpha.4",
"rrweb-player": "1.0.0-alpha.4",
"rrweb-snapshot": "2.0.0-alpha.4"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.84", "@git.zone/tsbuild": "^2.1.84",

79
pnpm-lock.yaml generated
View File

@ -26,6 +26,15 @@ importers:
'@social.io/interfaces': '@social.io/interfaces':
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.2.1 version: 1.2.1
rrweb:
specifier: 2.0.0-alpha.4
version: 2.0.0-alpha.4
rrweb-player:
specifier: 1.0.0-alpha.4
version: 1.0.0-alpha.4
rrweb-snapshot:
specifier: 2.0.0-alpha.4
version: 2.0.0-alpha.4
devDependencies: devDependencies:
'@git.zone/tsbuild': '@git.zone/tsbuild':
specifier: ^2.1.84 specifier: ^2.1.84
@ -690,6 +699,9 @@ packages:
resolution: {integrity: sha512-5SpUqD3X/2IZCTezCpk48Ss7cDc9QOuQAkeAYnJrRjDL4UCLakA3lBeHXRD/rsIB7S1smtXlayQ/vizfYzdbfw==} resolution: {integrity: sha512-5SpUqD3X/2IZCTezCpk48Ss7cDc9QOuQAkeAYnJrRjDL4UCLakA3lBeHXRD/rsIB7S1smtXlayQ/vizfYzdbfw==}
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smarttime deprecated: This package has been deprecated in favour of the new package at @push.rocks/smarttime
'@rrweb/types@2.0.0-alpha.18':
resolution: {integrity: sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==}
'@sec-ant/readable-stream@0.4.1': '@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@ -734,6 +746,9 @@ packages:
'@tsconfig/node16@1.0.4': '@tsconfig/node16@1.0.4':
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
'@tsconfig/svelte@1.0.13':
resolution: {integrity: sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==}
'@types/accepts@1.3.7': '@types/accepts@1.3.7':
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
@ -779,6 +794,9 @@ packages:
'@types/cors@2.8.17': '@types/cors@2.8.17':
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
'@types/css-font-loading-module@0.0.7':
resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==}
'@types/debounce@1.2.4': '@types/debounce@1.2.4':
resolution: {integrity: sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==} resolution: {integrity: sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==}
@ -991,6 +1009,9 @@ packages:
'@webcontainer/api@1.2.0': '@webcontainer/api@1.2.0':
resolution: {integrity: sha512-tzoKBd4lLdhHy5GHFpUkl+ndoSba8JqmB7x0ZQFnWfjbcbQOvKQfxA8MEMUYhgqjWHnbrWdAfnBEHz5f5lYG5A==} resolution: {integrity: sha512-tzoKBd4lLdhHy5GHFpUkl+ndoSba8JqmB7x0ZQFnWfjbcbQOvKQfxA8MEMUYhgqjWHnbrWdAfnBEHz5f5lYG5A==}
'@xstate/fsm@1.6.5':
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
'@yr/monotone-cubic-spline@1.0.3': '@yr/monotone-cubic-spline@1.0.3':
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
@ -1103,6 +1124,10 @@ packages:
resolution: {integrity: sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==} resolution: {integrity: sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
base64-js@1.5.1: base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@ -1605,6 +1630,9 @@ packages:
resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
figures@6.1.0: figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -2393,6 +2421,9 @@ packages:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mkdirp-classic@0.5.3: mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
@ -2785,6 +2816,18 @@ packages:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
hasBin: true hasBin: true
rrdom@0.1.7:
resolution: {integrity: sha512-ZLd8f14z9pUy2Hk9y636cNv5Y2BMnNEY99wxzW9tD2BLDfe1xFxtLjB4q/xCBYo6HRe0wofzKzjm4JojmpBfFw==}
rrweb-player@1.0.0-alpha.4:
resolution: {integrity: sha512-Wlmn9GZ5Fdqa37vd3TzsYdLl/JWEvXNUrLCrYpnOwEgmY409HwVIvvA5aIo7k582LoKgdRCsB87N+f0oWAR0Kg==}
rrweb-snapshot@2.0.0-alpha.4:
resolution: {integrity: sha512-KQ2OtPpXO5jLYqg1OnXS/Hf+EzqnZyP5A+XPqBCjYpj3XIje/Od4gdUwjbFo3cVuWq5Cw5Y1d3/xwgIS7/XpQQ==}
rrweb@2.0.0-alpha.4:
resolution: {integrity: sha512-wEHUILbxDPcNwkM3m4qgPgXAiBJyqCbbOHyVoNEVBJzHszWEFYyTbrZqUdeb1EfmTRC2PsumCIkVcomJ/xcOzA==}
rss-parser@3.13.0: rss-parser@3.13.0:
resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
@ -4549,6 +4592,8 @@ snapshots:
is-nan: 1.3.2 is-nan: 1.3.2
pretty-ms: 8.0.0 pretty-ms: 8.0.0
'@rrweb/types@2.0.0-alpha.18': {}
'@sec-ant/readable-stream@0.4.1': {} '@sec-ant/readable-stream@0.4.1': {}
'@sindresorhus/is@5.6.0': {} '@sindresorhus/is@5.6.0': {}
@ -4587,6 +4632,8 @@ snapshots:
'@tsconfig/node16@1.0.4': {} '@tsconfig/node16@1.0.4': {}
'@tsconfig/svelte@1.0.13': {}
'@types/accepts@1.3.7': '@types/accepts@1.3.7':
dependencies: dependencies:
'@types/node': 22.7.5 '@types/node': 22.7.5
@ -4639,6 +4686,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 22.7.5 '@types/node': 22.7.5
'@types/css-font-loading-module@0.0.7': {}
'@types/debounce@1.2.4': {} '@types/debounce@1.2.4': {}
'@types/debug@4.1.12': '@types/debug@4.1.12':
@ -4932,6 +4981,8 @@ snapshots:
'@webcontainer/api@1.2.0': {} '@webcontainer/api@1.2.0': {}
'@xstate/fsm@1.6.5': {}
'@yr/monotone-cubic-spline@1.0.3': {} '@yr/monotone-cubic-spline@1.0.3': {}
abbrev@1.1.1: abbrev@1.1.1:
@ -5030,6 +5081,8 @@ snapshots:
base64-arraybuffer-es6@0.7.0: {} base64-arraybuffer-es6@0.7.0: {}
base64-arraybuffer@1.0.2: {}
base64-js@1.5.1: {} base64-js@1.5.1: {}
base64id@2.0.0: {} base64id@2.0.0: {}
@ -5578,6 +5631,8 @@ snapshots:
dependencies: dependencies:
xml-js: 1.6.11 xml-js: 1.6.11
fflate@0.4.8: {}
figures@6.1.0: figures@6.1.0:
dependencies: dependencies:
is-unicode-supported: 2.1.0 is-unicode-supported: 2.1.0
@ -6635,6 +6690,8 @@ snapshots:
yallist: 4.0.0 yallist: 4.0.0
optional: true optional: true
mitt@3.0.1: {}
mkdirp-classic@0.5.3: {} mkdirp-classic@0.5.3: {}
mkdirp@1.0.4: {} mkdirp@1.0.4: {}
@ -7013,6 +7070,28 @@ snapshots:
dependencies: dependencies:
glob: 7.2.3 glob: 7.2.3
rrdom@0.1.7:
dependencies:
rrweb-snapshot: 2.0.0-alpha.4
rrweb-player@1.0.0-alpha.4:
dependencies:
'@tsconfig/svelte': 1.0.13
rrweb: 2.0.0-alpha.4
rrweb-snapshot@2.0.0-alpha.4: {}
rrweb@2.0.0-alpha.4:
dependencies:
'@rrweb/types': 2.0.0-alpha.18
'@types/css-font-loading-module': 0.0.7
'@xstate/fsm': 1.6.5
base64-arraybuffer: 1.0.2
fflate: 0.4.8
mitt: 3.0.1
rrdom: 0.1.7
rrweb-snapshot: 2.0.0-alpha.4
rss-parser@3.13.0: rss-parser@3.13.0:
dependencies: dependencies:
entities: 2.2.0 entities: 2.2.0

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@social.io/catalog', name: '@social.io/catalog',
version: '1.1.0', version: '1.2.0',
description: 'catalog for social.io' description: 'catalog for social.io'
} }

View File

@ -2,3 +2,4 @@ export * from './sio-fab.js';
export * from './sio-combox.js'; export * from './sio-combox.js';
export * from './sio-subwidget-onboardme.js'; export * from './sio-subwidget-onboardme.js';
export * from './sio-subwidget-conversations.js'; export * from './sio-subwidget-conversations.js';
export * from './sio-recorder.js';

View 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();
};
}