Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: merge events #1616

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/shy-garlics-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rrweb": patch
---

feat: Add support for merging events, you can choose any time range in replay and generate a new clip of the rrweb events which can be played independently even if there are no fullsnapshots in this time range.
14 changes: 14 additions & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
MaskInputFn,
MaskTextFn,
} from 'rrweb-snapshot';
import { snapshot } from 'rrweb-snapshot';
import type { IframeManager } from './record/iframe-manager';
import type { ShadowDomManager } from './record/shadow-dom-manager';
import type { Replayer } from './replay';
Expand Down Expand Up @@ -224,3 +225,16 @@ export type CrossOriginIframeMessageEvent =
MessageEvent<CrossOriginIframeMessageEventContent>;

export type ErrorHandler = (error: unknown) => void | boolean;

export type MergeOptions = {
// row events to deal with
events: eventWithTime[];
// options for take fullsnapshot, and mirror is required
snapshotOptions: Parameters<typeof snapshot>[1] & { mirror: Mirror };
// startTimeStamp to generate from
startTimeStamp: number;
// endTimeStamp to generate end, default: last timestamp in events
endTimeStamp?: number;
// replay iframe generated by Replayer, default .replayer-wrapper > iframe
iframe?: HTMLIFrameElement | null;
};
104 changes: 102 additions & 2 deletions packages/rrweb/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,20 @@
DeprecatedMirror,
textMutation,
IMirror,
serializedNodeWithId,
eventWithTime,
} from '@rrweb/types';
import type { Mirror, SlimDOMOptions } from 'rrweb-snapshot';
import { isShadowRoot, IGNORED_NODE, classMatchesRegex } from 'rrweb-snapshot';
import { EventType } from '@rrweb/types';
import { Mirror, type SlimDOMOptions } from 'rrweb-snapshot';
import {
isShadowRoot,
IGNORED_NODE,
classMatchesRegex,
snapshot,
} from 'rrweb-snapshot';
import { RRNode, RRIFrameElement, BaseRRNode } from 'rrdom';
import dom from '@rrweb/utils';
import type { MergeOptions } from './types';

export function on(
type: string,
Expand All @@ -32,7 +41,7 @@
'now you can use replayer.getMirror() to access the mirror instance of a replayer,' +
'\r\n' +
'or you can use record.mirror to access the mirror instance during recording.';
/** @deprecated */

Check warning on line 44 in packages/rrweb/src/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/utils.ts#L44

[tsdoc/syntax] tsdoc-missing-deprecation-message: The @deprecated block must include a deprecation message, e.g. describing the recommended alternative
export let _mirror: DeprecatedMirror = {
map: {},
getId() {
Expand Down Expand Up @@ -116,7 +125,7 @@
set(value) {
// put hooked setter into event loop to avoid of set latency
setTimeout(() => {
d.set!.call(this, value);

Check warning on line 128 in packages/rrweb/src/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/utils.ts#L128

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}, 0);
if (original && original.set) {
original.set.call(this, value);
Expand All @@ -129,7 +138,7 @@

// copy from https://github.com/getsentry/sentry-javascript/blob/b2109071975af8bf0316d3b5b38f519bdaf5dc15/packages/utils/src/object.ts
export function patch(
source: { [key: string]: any },

Check warning on line 141 in packages/rrweb/src/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/utils.ts#L141

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
name: string,
replacement: (...args: unknown[]) => unknown,
): () => void {
Expand Down Expand Up @@ -586,3 +595,94 @@
if (!doc) return false;
return dom.contains(doc, n) || shadowHostInDom(n);
}

/**
* You can choose any time range in Replay and generate a new clip of the rrweb events
* which can be played independently EVEN IF there are no fullsnapshots in this time range.
* @param options - merge options
* @returns merged events which can be played independently
*/
export function mergeEvents(options: MergeOptions) {
const { events, startTimeStamp, endTimeStamp, snapshotOptions } = options;

// check events
if (!events || !Array.isArray(events) || !events.length) {
console.warn('row events is required and not empty.');
return;
}

// check mirror
if (!snapshotOptions?.mirror) {
console.warn('mirror in snapshotOptions is required.');
return;
}

// check iframe
let iframe = options.iframe;
if (!iframe) {
const wrapper = document.querySelector('.replayer-wrapper');
if (wrapper) {
iframe = wrapper.querySelector('iframe');
}
}
if (!iframe) {
console.warn('iframe created by Replayer not found.');
return;
}
const doc = iframe.contentDocument;
const win = iframe.contentWindow;
if (!doc || !win) {
console.warn('contentDocument or contentWindow in iframe not found.');
return;
}

// take the fullsnapshot with the snapshotOptions
let node: serializedNodeWithId | null;
try {
// TODO iframe onSerialize, onIframeLoad, onStylesheetLoad, keepIframeSrcFn
node = snapshot(doc, snapshotOptions);
} catch (error) {
console.warn(error);
return;
}

const result: eventWithTime[] = [];
let lastMeta: eventWithTime | undefined;

for (let i = 0; i < events.length; i++) {
const event = events[i];
// record the last meta event
if (event.type === EventType.Meta) {
lastMeta = event;
}
if (event.timestamp < startTimeStamp) {
continue;
} else if (endTimeStamp && event.timestamp > endTimeStamp) {
break;
} else {
if (!result.length) {
if (!lastMeta) {
console.warn('Meta event not found in events');
return;
}
// append meta event
result.push({
...lastMeta,
timestamp: event.timestamp - 2,
});
// append fullsnapshot event
result.push({
type: EventType.FullSnapshot,
data: {
node,
initialOffset: getWindowScroll(win),
},
timestamp: event.timestamp - 1,
} as eventWithTime);
}
// append remains event
result.push(event);
}
}
return result;
}
Loading
Loading