From a10c2a1bda71afe1268853af8521f3a17643b500 Mon Sep 17 00:00:00 2001
From: eoghan <eoghan@getthere.ie>
Date: Tue, 26 May 2020 18:09:30 +0000
Subject: [PATCH 1/5] The `processMutations` function needed to be bound to the
 `mutationBuffer` object, as otherwise `this` referred to the
 `MutationObserver` object itself

---
 src/record/observer.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/record/observer.ts b/src/record/observer.ts
index 792cd7ff7f..27ff04d6d8 100644
--- a/src/record/observer.ts
+++ b/src/record/observer.ts
@@ -53,7 +53,9 @@ function initMutationObserver(
     maskInputOptions,
     recordCanvas,
   );
-  const observer = new MutationObserver(mutationBuffer.processMutations);
+  const observer = new MutationObserver(
+    mutationBuffer.processMutations.bind(mutationBuffer)
+  );
   observer.observe(document, {
     attributes: true,
     attributeOldValue: true,

From 321502ace93f8efe95735cba5e955a12f8ce0035 Mon Sep 17 00:00:00 2001
From: eoghan <eoghan@getthere.ie>
Date: Wed, 27 May 2020 16:25:58 +0000
Subject: [PATCH 2/5] Enable external pausing of mutation buffer emissions

 - no automatic pausing based on e.g. pageVisibility yet, assuming such a thing is desirable
   https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
 - user code has to call new API method `freezePage` e.g. when page is hidden or after a timeout
 - automatically unpauses when the next user initiated event occurs
   (am assuming everything that isn't a mutation event counts as 'user initiated'
   either way think this is the correct thing to do until I see a counterexample
   of an event that shouldn't cause the mutations to be unbufferred)
---
 src/record/index.ts          | 19 ++++++++++++++++++-
 src/record/mutation.ts       |  8 ++++++--
 src/record/observer.ts       |  6 ++++--
 typings/record/index.d.ts    |  1 +
 typings/record/mutation.d.ts |  3 ++-
 typings/record/observer.d.ts |  4 +++-
 6 files changed, 34 insertions(+), 7 deletions(-)

diff --git a/src/record/index.ts b/src/record/index.ts
index b5c508427a..fda4f32797 100644
--- a/src/record/index.ts
+++ b/src/record/index.ts
@@ -1,5 +1,5 @@
 import { snapshot, MaskInputOptions } from 'rrweb-snapshot';
-import initObservers from './observer';
+import { initObservers, mutationBuffer } from './observer';
 import {
   mirror,
   on,
@@ -81,6 +81,19 @@ function record<T = eventWithTime>(
   let lastFullSnapshotEvent: eventWithTime;
   let incrementalSnapshotCount = 0;
   wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => {
+    if (
+      mutationBuffer.paused &&
+      !(
+        e.type == EventType.IncrementalSnapshot &&
+        e.data.source == IncrementalSource.Mutation
+      )
+    ) {
+      // we've got a user initiated event so first we need to apply
+      // all DOM changes that have been buffering during paused state
+      mutationBuffer.emit();
+      mutationBuffer.paused = false;
+    }
+
     emit(((packFn ? packFn(e) : e) as unknown) as T, isCheckout);
     if (e.type === EventType.FullSnapshot) {
       lastFullSnapshotEvent = e;
@@ -325,4 +338,8 @@ record.addCustomEvent = <T>(tag: string, payload: T) => {
   );
 };
 
+record.freezePage = () => {
+  mutationBuffer.paused = true;
+};
+
 export default record;
diff --git a/src/record/mutation.ts b/src/record/mutation.ts
index ef781b9be7..5aa12260b2 100644
--- a/src/record/mutation.ts
+++ b/src/record/mutation.ts
@@ -109,6 +109,8 @@ function isINode(n: Node | INode): n is INode {
  * controls behaviour of a MutationObserver
  */
 export default class MutationBuffer {
+  public paused: boolean = false;
+
   private texts: textCursor[] = [];
   private attributes: attributeCursor[] = [];
   private removes: removedNodeMutation[] = [];
@@ -143,7 +145,7 @@ export default class MutationBuffer {
   private maskInputOptions: MaskInputOptions;
   private recordCanvas: boolean;
 
-  constructor(
+  public init(
     cb: mutationCallBack,
     blockClass: blockClass,
     inlineStylesheet: boolean,
@@ -253,7 +255,9 @@ export default class MutationBuffer {
       pushAdd(node.value);
     }
 
-    this.emit();
+    if (!this.paused) {
+      this.emit();
+    }
   };
 
   public emit = () => {
diff --git a/src/record/observer.ts b/src/record/observer.ts
index 27ff04d6d8..a557ce9216 100644
--- a/src/record/observer.ts
+++ b/src/record/observer.ts
@@ -38,6 +38,8 @@ import {
 } from '../types';
 import MutationBuffer from './mutation';
 
+export const mutationBuffer = new MutationBuffer();
+
 function initMutationObserver(
   cb: mutationCallBack,
   blockClass: blockClass,
@@ -46,7 +48,7 @@ function initMutationObserver(
   recordCanvas: boolean,
 ): MutationObserver {
   // see mutation.ts for details
-  const mutationBuffer = new MutationBuffer(
+  mutationBuffer.init(
     cb,
     blockClass,
     inlineStylesheet,
@@ -562,7 +564,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
   };
 }
 
-export default function initObservers(
+export function initObservers(
   o: observerParam,
   hooks: hooksParam = {},
 ): listenerHandler {
diff --git a/typings/record/index.d.ts b/typings/record/index.d.ts
index 73e1f2c373..9c5e20436f 100644
--- a/typings/record/index.d.ts
+++ b/typings/record/index.d.ts
@@ -2,5 +2,6 @@ import { eventWithTime, recordOptions, listenerHandler } from '../types';
 declare function record<T = eventWithTime>(options?: recordOptions<T>): listenerHandler | undefined;
 declare namespace record {
     var addCustomEvent: <T>(tag: string, payload: T) => void;
+    var freezePage: () => void;
 }
 export default record;
diff --git a/typings/record/mutation.d.ts b/typings/record/mutation.d.ts
index 9d92fd205e..f8e5a9c0b5 100644
--- a/typings/record/mutation.d.ts
+++ b/typings/record/mutation.d.ts
@@ -1,6 +1,7 @@
 import { MaskInputOptions } from 'rrweb-snapshot';
 import { mutationRecord, blockClass, mutationCallBack } from '../types';
 export default class MutationBuffer {
+    paused: boolean;
     private texts;
     private attributes;
     private removes;
@@ -14,7 +15,7 @@ export default class MutationBuffer {
     private inlineStylesheet;
     private maskInputOptions;
     private recordCanvas;
-    constructor(cb: mutationCallBack, blockClass: blockClass, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, recordCanvas: boolean);
+    init(cb: mutationCallBack, blockClass: blockClass, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, recordCanvas: boolean): void;
     processMutations: (mutations: mutationRecord[]) => void;
     emit: () => void;
     private processMutation;
diff --git a/typings/record/observer.d.ts b/typings/record/observer.d.ts
index 75cac487be..9958737880 100644
--- a/typings/record/observer.d.ts
+++ b/typings/record/observer.d.ts
@@ -1,3 +1,5 @@
 import { observerParam, listenerHandler, hooksParam } from '../types';
+import MutationBuffer from './mutation';
+export declare const mutationBuffer: MutationBuffer;
 export declare const INPUT_TAGS: string[];
-export default function initObservers(o: observerParam, hooks?: hooksParam): listenerHandler;
+export declare function initObservers(o: observerParam, hooks?: hooksParam): listenerHandler;

From 18a6a5a18ef197b0e30a3a9fd7c27bb9ffb79eda Mon Sep 17 00:00:00 2001
From: eoghan <eoghan@getthere.ie>
Date: Tue, 9 Jun 2020 15:13:31 +0000
Subject: [PATCH 3/5] Avoid a build up of duplicate `adds` by delaying pushing
 to adds until emission time

---
 src/record/mutation.ts | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/record/mutation.ts b/src/record/mutation.ts
index 5aa12260b2..7366e3d8a6 100644
--- a/src/record/mutation.ts
+++ b/src/record/mutation.ts
@@ -114,7 +114,6 @@ export default class MutationBuffer {
   private texts: textCursor[] = [];
   private attributes: attributeCursor[] = [];
   private removes: removedNodeMutation[] = [];
-  private adds: addedNodeMutation[] = [];
 
   private movedMap: Record<string, true> = {};
 
@@ -161,6 +160,14 @@ export default class MutationBuffer {
 
   public processMutations = (mutations: mutationRecord[]) => {
     mutations.forEach(this.processMutation);
+    if (!this.paused) {
+      this.emit();
+    }
+  };
+
+  public emit = () => {
+
+    const adds: addedNodeMutation[] = [];
 
     /**
      * Sometimes child node may be pushed before its newly added
@@ -184,7 +191,7 @@ export default class MutationBuffer {
       if (parentId === -1 || nextId === -1) {
         return addList.addNode(n);
       }
-      this.adds.push({
+      adds.push({
         parentId,
         nextId,
         node: serializeNodeWithId(
@@ -255,12 +262,6 @@ export default class MutationBuffer {
       pushAdd(node.value);
     }
 
-    if (!this.paused) {
-      this.emit();
-    }
-  };
-
-  public emit = () => {
     const payload = {
       texts: this.texts
         .map((text) => ({
@@ -277,7 +278,7 @@ export default class MutationBuffer {
         // attribute mutation's id was not in the mirror map means the target node has been removed
         .filter((attribute) => mirror.has(attribute.id)),
       removes: this.removes,
-      adds: this.adds,
+      adds: adds,
     };
     // payload may be empty if the mutations happened in some blocked elements
     if (
@@ -294,7 +295,6 @@ export default class MutationBuffer {
     this.texts = [];
     this.attributes = [];
     this.removes = [];
-    this.adds = [];
     this.addedSet = new Set<Node>();
     this.movedSet = new Set<Node>();
     this.droppedSet = new Set<Node>();

From fcc290d496b5b9189901b98c3da7a59350b9fd4d Mon Sep 17 00:00:00 2001
From: Eoghan Murray <eoghan@getthere.ie>
Date: Fri, 12 Jun 2020 18:07:26 +0100
Subject: [PATCH 4/5] Need to export freezePage in order to use it from
 rrweb.min.js

---
 src/index.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/index.ts b/src/index.ts
index 7883678d9a..e57b8e42ba 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,5 +11,6 @@ export {
 } from './types';
 
 const { addCustomEvent } = record;
+const { freezePage } = record;
 
-export { record, addCustomEvent, Replayer, mirror, utils };
+export { record, addCustomEvent, freezePage, Replayer, mirror, utils };

From e8fcfcc3dd0fbe834935c08ea21c4dda64e4e002 Mon Sep 17 00:00:00 2001
From: Eoghan Murray <eoghan@getthere.ie>
Date: Fri, 12 Jun 2020 18:29:05 +0100
Subject: [PATCH 5/5] Add a test to check if mutations can be turned off with
 the `freezePage` method

---
 test/__snapshots__/integration.test.ts.snap | 167 ++++++++++++++++++++
 test/integration.test.ts                    |  26 +++
 2 files changed, 193 insertions(+)

diff --git a/test/__snapshots__/integration.test.ts.snap b/test/__snapshots__/integration.test.ts.snap
index 18875bd175..3f89221028 100644
--- a/test/__snapshots__/integration.test.ts.snap
+++ b/test/__snapshots__/integration.test.ts.snap
@@ -1561,6 +1561,173 @@ exports[`form 1`] = `
 ]"
 `;
 
+exports[`frozen 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\\": 2,
+            \\"tagName\\": \\"html\\",
+            \\"attributes\\": {},
+            \\"childNodes\\": [
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"head\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [],
+                \\"id\\": 3
+              },
+              {
+                \\"type\\": 2,
+                \\"tagName\\": \\"body\\",
+                \\"attributes\\": {},
+                \\"childNodes\\": [
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 5
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"p\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"mutation observer\\",
+                        \\"id\\": 7
+                      }
+                    ],
+                    \\"id\\": 6
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n  \\",
+                    \\"id\\": 8
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"ul\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\\\n    \\",
+                        \\"id\\": 10
+                      },
+                      {
+                        \\"type\\": 2,
+                        \\"tagName\\": \\"li\\",
+                        \\"attributes\\": {},
+                        \\"childNodes\\": [],
+                        \\"id\\": 11
+                      },
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"\\\\n  \\",
+                        \\"id\\": 12
+                      }
+                    ],
+                    \\"id\\": 9
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n\\\\n    \\",
+                    \\"id\\": 13
+                  },
+                  {
+                    \\"type\\": 2,
+                    \\"tagName\\": \\"script\\",
+                    \\"attributes\\": {},
+                    \\"childNodes\\": [
+                      {
+                        \\"type\\": 3,
+                        \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+                        \\"id\\": 15
+                      }
+                    ],
+                    \\"id\\": 14
+                  },
+                  {
+                    \\"type\\": 3,
+                    \\"textContent\\": \\"\\\\n    \\\\n    \\",
+                    \\"id\\": 16
+                  }
+                ],
+                \\"id\\": 4
+              }
+            ],
+            \\"id\\": 2
+          }
+        ],
+        \\"id\\": 1
+      },
+      \\"initialOffset\\": {
+        \\"left\\": 0,
+        \\"top\\": 0
+      }
+    }
+  },
+  {
+    \\"type\\": 3,
+    \\"data\\": {
+      \\"source\\": 0,
+      \\"texts\\": [],
+      \\"attributes\\": [
+        {
+          \\"id\\": 17,
+          \\"attributes\\": {
+            \\"foo\\": \\"bar\\"
+          }
+        },
+        {
+          \\"id\\": 4,
+          \\"attributes\\": {
+            \\"test\\": \\"true\\"
+          }
+        }
+      ],
+      \\"removes\\": [],
+      \\"adds\\": [
+        {
+          \\"parentId\\": 9,
+          \\"nextId\\": null,
+          \\"node\\": {
+            \\"type\\": 2,
+            \\"tagName\\": \\"li\\",
+            \\"attributes\\": {
+              \\"foo\\": \\"bar\\"
+            },
+            \\"childNodes\\": [],
+            \\"id\\": 17
+          }
+        }
+      ]
+    }
+  }
+]"
+`;
+
 exports[`ignore 1`] = `
 "[
   {
diff --git a/test/integration.test.ts b/test/integration.test.ts
index f308109cfe..394b4207f6 100644
--- a/test/integration.test.ts
+++ b/test/integration.test.ts
@@ -139,6 +139,32 @@ describe('record integration tests', function (this: ISuite) {
     assertSnapshot(snapshots, __filename, 'select2');
   });
 
+  it('can freeze mutations', async () => {
+    const page: puppeteer.Page = await this.browser.newPage();
+    await page.goto('about:blank');
+    await page.setContent(getHtml.call(this, 'mutation-observer.html'));
+
+    await page.evaluate(() => {
+      const li = document.createElement('li');
+      const ul = document.querySelector('ul') as HTMLUListElement;
+      ul.appendChild(li);
+      li.setAttribute('foo', 'bar');
+      document.body.setAttribute('test', 'true');
+    });
+    await page.evaluate('rrweb.freezePage()');
+    await page.evaluate(() => {
+      document.body.setAttribute('test', 'bad');
+      const ul = document.querySelector('ul') as HTMLUListElement;
+      const li = document.createElement('li');
+      li.setAttribute('bad-attr', 'bad');
+      li.innerText = 'bad text';
+      ul.appendChild(li);
+      document.body.removeChild(ul);
+    });
+    const snapshots = await page.evaluate('window.snapshots');
+    assertSnapshot(snapshots, __filename, 'frozen');
+  });
+
   it('should not record input events on ignored elements', async () => {
     const page: puppeteer.Page = await this.browser.newPage();
     await page.goto('about:blank');