Skip to content

Commit

Permalink
feat: Add onMutation option to record (#70)
Browse files Browse the repository at this point in the history
When this callback returns `false`, we skip handling the mutations
normally. It's up to the host app to do something with this then.
  • Loading branch information
mydea authored and billyvg committed Jul 29, 2023
1 parent 0fb0d29 commit 8767b24
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 41 deletions.
3 changes: 3 additions & 0 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ function record<T = eventWithTime>(
keepIframeSrcFn = () => false,
ignoreCSSAttributes = new Set([]),
errorHandler,
onMutation,
} = options;

registerErrorHandler(errorHandler);
Expand Down Expand Up @@ -327,6 +328,7 @@ function record<T = eventWithTime>(
mutationCb: wrappedMutationEmit,
scrollCb: wrappedScrollEmit,
bypassOptions: {
onMutation,
blockClass,
blockSelector,
maskAllText,
Expand Down Expand Up @@ -441,6 +443,7 @@ function record<T = eventWithTime>(
const observe = (doc: Document) => {
return callbackWrapper(initObservers)(
{
onMutation,
mutationCb: wrappedMutationEmit,
mousemoveCb: (positions, source) =>
wrappedEmit(
Expand Down
9 changes: 8 additions & 1 deletion packages/rrweb/src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,14 @@ export function initMutationObserver(
const observer = new (mutationObserverCtor as new (
callback: MutationCallback,
) => MutationObserver)(
callbackWrapper(mutationBuffer.processMutations.bind(mutationBuffer)),
callbackWrapper((mutations) => {
// If this callback returns `false`, we do not want to process the mutations
// This can be used to e.g. do a manual full snapshot when mutations become too large, or similar.
if (options.onMutation && options.onMutation(mutations) === false) {
return;
}
mutationBuffer.processMutations.bind(mutationBuffer)(mutations);
}),
);
observer.observe(rootEl, {
attributes: true,
Expand Down
3 changes: 3 additions & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ export type recordOptions<T> = {
mousemoveWait?: number;
keepIframeSrcFn?: KeepIframeSrcFn;
errorHandler?: ErrorHandler;
onMutation?: (mutations: MutationRecord[]) => boolean,
};

export type observerParam = {
onMutation?: (mutations: MutationRecord[]) => boolean,
mutationCb: mutationCallBack;
mousemoveCb: mousemoveCallBack;
mouseInteractionCb: mouseInteractionCallBack;
Expand Down Expand Up @@ -135,6 +137,7 @@ export type observerParam = {

export type MutationBufferParam = Pick<
observerParam,
| 'onMutation'
| 'mutationCb'
| 'blockClass'
| 'blockSelector'
Expand Down
197 changes: 157 additions & 40 deletions packages/rrweb/test/__snapshots__/integration.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,154 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`record integration tests can configure onMutation 1`] = `
"[
{
\\"type\\": 0,
\\"data\\": {}
},
{
\\"type\\": 1,
\\"data\\": {}
},
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 1,
\\"name\\": \\"html\\",
\\"publicId\\": \\"\\",
\\"systemId\\": \\"\\",
\\"id\\": 2
},
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 4
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 6
},
{
\\"type\\": 2,
\\"tagName\\": \\"p\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"mutation observer\\",
\\"id\\": 8
}
],
\\"id\\": 7
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 9
},
{
\\"type\\": 2,
\\"tagName\\": \\"ul\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 11
},
{
\\"type\\": 2,
\\"tagName\\": \\"li\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 12
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 13
}
],
\\"id\\": 10
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 14
},
{
\\"type\\": 2,
\\"tagName\\": \\"canvas\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 15
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n \\",
\\"id\\": 16
},
{
\\"type\\": 2,
\\"tagName\\": \\"script\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 18
}
],
\\"id\\": 17
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\",
\\"id\\": 19
}
],
\\"id\\": 5
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
}
]"
`;

exports[`record integration tests can correctly serialize a shader and multiple webgl contexts 1`] = `
"[
{
Expand Down Expand Up @@ -3582,14 +3731,6 @@ exports[`record integration tests can record node mutations 1`] = `
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 70
}
},
{
\\"type\\": 3,
\\"data\\": {
Expand All @@ -3608,6 +3749,14 @@ exports[`record integration tests can record node mutations 1`] = `
\\"id\\": 35
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 70
}
},
{
\\"type\\": 3,
\\"data\\": {
Expand Down Expand Up @@ -9158,14 +9307,6 @@ exports[`record integration tests should nest record iframe 1`] = `
\\"attributes\\": [],
\\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 4,
\\"width\\": 1920,
\\"height\\": 1080
}
}
]"
`;
Expand Down Expand Up @@ -13802,14 +13943,6 @@ exports[`record integration tests should record images inside iframe with blob u
\\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 4,
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 3,
\\"data\\": {
Expand Down Expand Up @@ -16301,14 +16434,6 @@ exports[`record integration tests should record mutations in iframes accross pag
\\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 4,
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 3,
\\"data\\": {
Expand Down Expand Up @@ -16702,14 +16827,6 @@ exports[`record integration tests should record nested iframes and shadow doms 1
\\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 4,
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 3,
\\"data\\": {
Expand Down
30 changes: 30 additions & 0 deletions packages/rrweb/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,36 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots);
});

it('can configure onMutation', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');

await page.setContent(
getHtml.call(this, 'mutation-observer.html', {
// @ts-expect-error Need to stringify this for tests
onMutation: `(mutations) => { window.lastMutationsLength = mutations.length; return mutations.length < 500 }`,
}),
);

await page.evaluate(() => {
const ul = document.querySelector('ul') as HTMLUListElement;

for(let i = 0; i < 2000; i++) {
const li = document.createElement('li');
ul.appendChild(li);
const p = document.querySelector('p') as HTMLParagraphElement;
p.appendChild(document.createElement('span'));
}
});

const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots);

const lastMutationsLength = await page.evaluate('window.lastMutationsLength');
expect(lastMutationsLength).toBe(4000);
});


it('can freeze mutations', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
Expand Down
1 change: 1 addition & 0 deletions packages/rrweb/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ export function generateRecordSnippet(options: recordOptions<eventWithTime>) {
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
maskInputFn: ${options.maskInputFn},
userTriggeredOnInput: ${options.userTriggeredOnInput},
onMutation: ${options.onMutation || undefined},
maskAttributeFn: ${options.maskAttributeFn},
maskTextFn: ${options.maskTextFn},
maskInputFn: ${options.maskInputFn},
Expand Down

0 comments on commit 8767b24

Please sign in to comment.