Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use case a user should call freeze page manually?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usecase for Statcounter is that some pages have a constant stream of mutations happening all the time (an image carousel where transitions are controlled by javascript rather than CSS). These generate huge recordings. We've mitigated this by deciding that if there is no user activity on the page, we call freezePage after a timeout.

A second thing we do is to call freezePage when the page loses focus (document.addEventListener('visibilitychange')).

So I've gone with implementing this as a function call, rather than as a setting (autoFreezeAfterSeconds or similar), as there may be other use cases I haven't thought of, and I felt like there would need to be two config settings for the above two scenarios, which felt like a harder thing to document.

See https://statcounter.com/counter/recorder_uncompressed.js for details on how the two calls are set up; these could be added to documentation as examples if we're gonna keep the freezePage method approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot open the about link but I do understand the use case now. Thanks!

27 changes: 26 additions & 1 deletion src/record/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { snapshot, MaskInputOptions } from 'rrweb-snapshot';
import initObservers from './observer';
import { initObservers, mutationBuffer } from './observer';
import {
mirror,
on,
Expand Down Expand Up @@ -81,6 +81,20 @@ function record<T = eventWithTime>(
let lastFullSnapshotEvent: eventWithTime;
let incrementalSnapshotCount = 0;
wrappedEmit = (e: eventWithTime, isCheckout?: boolean) => {
if (
mutationBuffer.isFrozen() &&
e.type !== EventType.FullSnapshot &&
!(
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.unfreeze();
}

emit(((packFn ? packFn(e) : e) as unknown) as T, isCheckout);
if (e.type === EventType.FullSnapshot) {
lastFullSnapshotEvent = e;
Expand Down Expand Up @@ -110,6 +124,9 @@ function record<T = eventWithTime>(
}),
isCheckout,
);

let wasFrozen = mutationBuffer.isFrozen();
mutationBuffer.freeze(); // don't allow any mirror modifications during snapshotting
const [node, idNodeMap] = snapshot(
document,
blockClass,
Expand Down Expand Up @@ -147,6 +164,10 @@ function record<T = eventWithTime>(
},
}),
);
if (!wasFrozen) {
mutationBuffer.emit(); // emit anything queued up now
mutationBuffer.unfreeze();
}
}

try {
Expand Down Expand Up @@ -325,4 +346,8 @@ record.addCustomEvent = <T>(tag: string, payload: T) => {
);
};

record.freezePage = () => {
mutationBuffer.freeze();
};

export default record;
46 changes: 35 additions & 11 deletions src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,12 @@ function isINode(n: Node | INode): n is INode {
* controls behaviour of a MutationObserver
*/
export default class MutationBuffer {
private frozen: boolean = false;

private texts: textCursor[] = [];
private attributes: attributeCursor[] = [];
private removes: removedNodeMutation[] = [];
private adds: addedNodeMutation[] = [];
private mapRemoves: Node[] = [];

private movedMap: Record<string, true> = {};

Expand Down Expand Up @@ -143,7 +145,7 @@ export default class MutationBuffer {
private maskInputOptions: MaskInputOptions;
private recordCanvas: boolean;

constructor(
public init(
cb: mutationCallBack,
blockClass: blockClass,
inlineStylesheet: boolean,
Expand All @@ -157,8 +159,30 @@ export default class MutationBuffer {
this.emissionCallback = cb;
}

public freeze() {
this.frozen = true;
}

public unfreeze() {
this.frozen = false;
}

public isFrozen() {
return this.frozen;
}

public processMutations = (mutations: mutationRecord[]) => {
mutations.forEach(this.processMutation);
if (!this.frozen) {
this.emit();
}
};

public emit = () => {
// delay any modification of the mirror until this function
// so that the mirror for takeFullSnapshot doesn't get mutated while it's event is being processed

const adds: addedNodeMutation[] = [];

/**
* Sometimes child node may be pushed before its newly added
Expand All @@ -182,7 +206,7 @@ export default class MutationBuffer {
if (parentId === -1 || nextId === -1) {
return addList.addNode(n);
}
this.adds.push({
adds.push({
parentId,
nextId,
node: serializeNodeWithId(
Expand All @@ -198,6 +222,10 @@ export default class MutationBuffer {
});
};

while (this.mapRemoves.length) {
mirror.removeNodeFromMap(this.mapRemoves.shift() as INode);
}

for (const n of this.movedSet) {
pushAdd(n);
}
Expand Down Expand Up @@ -253,10 +281,6 @@ export default class MutationBuffer {
pushAdd(node.value);
}

this.emit();
};

public emit = () => {
const payload = {
texts: this.texts
.map((text) => ({
Expand All @@ -273,7 +297,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 (
Expand All @@ -284,17 +308,17 @@ export default class MutationBuffer {
) {
return;
}
this.emissionCallback(payload);

// reset
this.texts = [];
this.attributes = [];
this.removes = [];
this.adds = [];
this.addedSet = new Set<Node>();
this.movedSet = new Set<Node>();
this.droppedSet = new Set<Node>();
this.movedMap = {};

this.emissionCallback(payload);
};

private processMutation = (m: mutationRecord) => {
Expand Down Expand Up @@ -373,7 +397,7 @@ export default class MutationBuffer {
id: nodeId,
});
}
mirror.removeNodeFromMap(n as INode);
this.mapRemoves.push(n);
});
break;
}
Expand Down
10 changes: 7 additions & 3 deletions src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import {
} from '../types';
import MutationBuffer from './mutation';

export const mutationBuffer = new MutationBuffer();

function initMutationObserver(
cb: mutationCallBack,
blockClass: blockClass,
Expand All @@ -46,14 +48,16 @@ function initMutationObserver(
recordCanvas: boolean,
): MutationObserver {
// see mutation.ts for details
const mutationBuffer = new MutationBuffer(
mutationBuffer.init(
cb,
blockClass,
inlineStylesheet,
maskInputOptions,
recordCanvas,
);
const observer = new MutationObserver(mutationBuffer.processMutations);
const observer = new MutationObserver(
mutationBuffer.processMutations.bind(mutationBuffer)
);
observer.observe(document, {
attributes: true,
attributeOldValue: true,
Expand Down Expand Up @@ -560,7 +564,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
};
}

export default function initObservers(
export function initObservers(
o: observerParam,
hooks: hooksParam = {},
): listenerHandler {
Expand Down
Loading