rrweb
Version:
record and replay the web
776 lines (773 loc) • 32.1 kB
JavaScript
import { __assign, __values, __read } from '../../node_modules/tslib/tslib.es6.js';
import { rebuild, NodeType, buildNodeWithSN } from '../../node_modules/rrweb-snapshot/es/rrweb-snapshot.js';
import { ReplayerEvents, EventType, IncrementalSource, MediaInteractions, MouseInteractions } from '../types.js';
import { mirror, polyfill, TreeIndex } from '../utils.js';
import * as mittProxy from '../../node_modules/mitt/dist/mitt.es.js';
import mitt$1 from '../../node_modules/mitt/dist/mitt.es.js';
import { polyfill as smoothscroll_1 } from '../../node_modules/smoothscroll-polyfill/dist/smoothscroll.js';
import { Timer } from './timer.js';
import { createPlayerService, createSpeedService } from './machine.js';
import getInjectStyleRules from './styles/inject-style.js';
var SKIP_TIME_THRESHOLD = 10 * 1000;
var SKIP_TIME_INTERVAL = 5 * 1000;
var mitt = mitt$1 || mittProxy;
var REPLAY_CONSOLE_PREFIX = '[replayer]';
var defaultConfig = {
speed: 1,
root: document.body,
loadTimeout: 0,
skipInactive: false,
showWarning: true,
showDebug: false,
blockClass: 'rr-block',
liveMode: false,
insertStyleRules: [],
triggerFocus: true,
};
var Replayer = (function () {
function Replayer(events, config) {
var _this = this;
this.emitter = mitt();
this.legacy_missingNodeRetryMap = {};
if (!(config === null || config === void 0 ? void 0 : config.liveMode) && events.length < 2) {
throw new Error('Replayer need at least 2 events.');
}
this.config = Object.assign({}, defaultConfig, config);
this.handleResize = this.handleResize.bind(this);
this.getCastFn = this.getCastFn.bind(this);
this.emitter.on(ReplayerEvents.Resize, this.handleResize);
smoothscroll_1();
polyfill();
this.setupDom();
this.treeIndex = new TreeIndex();
this.fragmentParentMap = new Map();
this.emitter.on(ReplayerEvents.Flush, function () {
var e_1, _a, e_2, _b, e_3, _c;
var _d = _this.treeIndex.flush(), scrollMap = _d.scrollMap, inputMap = _d.inputMap;
try {
for (var _e = __values(scrollMap.values()), _f = _e.next(); !_f.done; _f = _e.next()) {
var d = _f.value;
_this.applyScroll(d);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_f && !_f.done && (_a = _e.return)) _a.call(_e);
}
finally { if (e_1) throw e_1.error; }
}
try {
for (var _g = __values(inputMap.values()), _h = _g.next(); !_h.done; _h = _g.next()) {
var d = _h.value;
_this.applyInput(d);
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (_h && !_h.done && (_b = _g.return)) _b.call(_g);
}
finally { if (e_2) throw e_2.error; }
}
try {
for (var _j = __values(_this.fragmentParentMap.entries()), _k = _j.next(); !_k.done; _k = _j.next()) {
var _l = __read(_k.value, 2), frag = _l[0], parent = _l[1];
mirror.map[parent.__sn.id] = parent;
if (parent.__sn.type === NodeType.Element &&
parent.__sn.tagName === 'textarea' &&
frag.textContent) {
parent.value = frag.textContent;
}
parent.appendChild(frag);
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_k && !_k.done && (_c = _j.return)) _c.call(_j);
}
finally { if (e_3) throw e_3.error; }
}
_this.fragmentParentMap.clear();
});
var timer = new Timer([], (config === null || config === void 0 ? void 0 : config.speed) || defaultConfig.speed);
this.service = createPlayerService({
events: events.map(function (e) {
if (config && config.unpackFn) {
return config.unpackFn(e);
}
return e;
}),
timer: timer,
timeOffset: 0,
baselineTime: 0,
lastPlayedEvent: null,
}, {
getCastFn: this.getCastFn,
emitter: this.emitter,
});
this.service.start();
this.service.subscribe(function (state) {
_this.emitter.emit(ReplayerEvents.StateChange, {
player: state,
});
});
this.speedService = createSpeedService({
normalSpeed: -1,
timer: timer,
});
this.speedService.start();
this.speedService.subscribe(function (state) {
_this.emitter.emit(ReplayerEvents.StateChange, {
speed: state,
});
});
var firstMeta = this.service.state.context.events.find(function (e) { return e.type === EventType.Meta; });
var firstFullsnapshot = this.service.state.context.events.find(function (e) { return e.type === EventType.FullSnapshot; });
if (firstMeta) {
var _a = firstMeta.data, width_1 = _a.width, height_1 = _a.height;
setTimeout(function () {
_this.emitter.emit(ReplayerEvents.Resize, {
width: width_1,
height: height_1,
});
}, 0);
}
if (firstFullsnapshot) {
this.rebuildFullSnapshot(firstFullsnapshot);
}
}
Object.defineProperty(Replayer.prototype, "timer", {
get: function () {
return this.service.state.context.timer;
},
enumerable: false,
configurable: true
});
Replayer.prototype.on = function (event, handler) {
this.emitter.on(event, handler);
};
Replayer.prototype.setConfig = function (config) {
var _this = this;
Object.keys(config).forEach(function (key) {
_this.config[key] = config[key];
});
if (!this.config.skipInactive) {
this.backToNormal();
}
};
Replayer.prototype.getMetaData = function () {
var firstEvent = this.service.state.context.events[0];
var lastEvent = this.service.state.context.events[this.service.state.context.events.length - 1];
return {
startTime: firstEvent.timestamp,
endTime: lastEvent.timestamp,
totalTime: lastEvent.timestamp - firstEvent.timestamp,
};
};
Replayer.prototype.getCurrentTime = function () {
return this.timer.timeOffset + this.getTimeOffset();
};
Replayer.prototype.getTimeOffset = function () {
var _a = this.service.state.context, baselineTime = _a.baselineTime, events = _a.events;
return baselineTime - events[0].timestamp;
};
Replayer.prototype.play = function (timeOffset) {
if (timeOffset === void 0) { timeOffset = 0; }
if (this.service.state.matches('paused')) {
this.service.send({ type: 'PLAY', payload: { timeOffset: timeOffset } });
}
else {
this.service.send({ type: 'PAUSE' });
this.service.send({ type: 'PLAY', payload: { timeOffset: timeOffset } });
}
this.emitter.emit(ReplayerEvents.Start);
};
Replayer.prototype.pause = function (timeOffset) {
if (timeOffset === undefined && this.service.state.matches('playing')) {
this.service.send({ type: 'PAUSE' });
}
if (typeof timeOffset === 'number') {
this.play(timeOffset);
this.service.send({ type: 'PAUSE' });
}
this.emitter.emit(ReplayerEvents.Pause);
};
Replayer.prototype.resume = function (timeOffset) {
if (timeOffset === void 0) { timeOffset = 0; }
console.warn("The 'resume' will be departed in 1.0. Please use 'play' method which has the same interface.");
this.play(timeOffset);
this.emitter.emit(ReplayerEvents.Resume);
};
Replayer.prototype.startLive = function (baselineTime) {
this.service.send({ type: 'TO_LIVE', payload: { baselineTime: baselineTime } });
};
Replayer.prototype.addEvent = function (rawEvent) {
var _this = this;
var event = this.config.unpackFn
? this.config.unpackFn(rawEvent)
: rawEvent;
Promise.resolve().then(function () {
return _this.service.send({ type: 'ADD_EVENT', payload: { event: event } });
});
};
Replayer.prototype.enableInteract = function () {
this.iframe.setAttribute('scrolling', 'auto');
this.iframe.style.pointerEvents = 'auto';
};
Replayer.prototype.disableInteract = function () {
this.iframe.setAttribute('scrolling', 'no');
this.iframe.style.pointerEvents = 'none';
};
Replayer.prototype.setupDom = function () {
this.wrapper = document.createElement('div');
this.wrapper.classList.add('replayer-wrapper');
this.config.root.appendChild(this.wrapper);
this.mouse = document.createElement('div');
this.mouse.classList.add('replayer-mouse');
this.wrapper.appendChild(this.mouse);
this.iframe = document.createElement('iframe');
this.iframe.setAttribute('sandbox', 'allow-same-origin');
this.disableInteract();
this.wrapper.appendChild(this.iframe);
};
Replayer.prototype.handleResize = function (dimension) {
this.iframe.setAttribute('width', String(dimension.width));
this.iframe.setAttribute('height', String(dimension.height));
};
Replayer.prototype.getCastFn = function (event, isSync) {
var _this = this;
if (isSync === void 0) { isSync = false; }
var castFn;
switch (event.type) {
case EventType.DomContentLoaded:
case EventType.Load:
break;
case EventType.Custom:
castFn = function () {
_this.emitter.emit(ReplayerEvents.CustomEvent, event);
};
break;
case EventType.Meta:
castFn = function () {
return _this.emitter.emit(ReplayerEvents.Resize, {
width: event.data.width,
height: event.data.height,
});
};
break;
case EventType.FullSnapshot:
castFn = function () {
_this.rebuildFullSnapshot(event);
_this.iframe.contentWindow.scrollTo(event.data.initialOffset);
};
break;
case EventType.IncrementalSnapshot:
castFn = function () {
var e_4, _a;
_this.applyIncremental(event, isSync);
if (isSync) {
return;
}
if (event === _this.nextUserInteractionEvent) {
_this.nextUserInteractionEvent = null;
_this.backToNormal();
}
if (_this.config.skipInactive && !_this.nextUserInteractionEvent) {
try {
for (var _b = __values(_this.service.state.context.events), _c = _b.next(); !_c.done; _c = _b.next()) {
var _event = _c.value;
if (_event.timestamp <= event.timestamp) {
continue;
}
if (_this.isUserInteraction(_event)) {
if (_event.delay - event.delay >
SKIP_TIME_THRESHOLD *
_this.speedService.state.context.timer.speed) {
_this.nextUserInteractionEvent = _event;
}
break;
}
}
}
catch (e_4_1) { e_4 = { error: e_4_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
}
finally { if (e_4) throw e_4.error; }
}
if (_this.nextUserInteractionEvent) {
var skipTime = _this.nextUserInteractionEvent.delay - event.delay;
var payload = {
speed: Math.min(Math.round(skipTime / SKIP_TIME_INTERVAL), 360),
};
_this.speedService.send({ type: 'FAST_FORWARD', payload: payload });
_this.emitter.emit(ReplayerEvents.SkipStart, payload);
}
}
};
break;
}
var wrappedCastFn = function () {
if (castFn) {
castFn();
}
_this.service.send({ type: 'CAST_EVENT', payload: { event: event } });
if (event ===
_this.service.state.context.events[_this.service.state.context.events.length - 1]) {
_this.backToNormal();
_this.service.send('END');
_this.emitter.emit(ReplayerEvents.Finish);
}
};
return wrappedCastFn;
};
Replayer.prototype.rebuildFullSnapshot = function (event) {
if (!this.iframe.contentDocument) {
return console.warn('Looks like your replayer has been destroyed.');
}
if (Object.keys(this.legacy_missingNodeRetryMap).length) {
console.warn('Found unresolved missing node map', this.legacy_missingNodeRetryMap);
}
this.legacy_missingNodeRetryMap = {};
mirror.map = rebuild(event.data.node, this.iframe.contentDocument)[1];
var styleEl = document.createElement('style');
var _a = this.iframe.contentDocument, documentElement = _a.documentElement, head = _a.head;
documentElement.insertBefore(styleEl, head);
var injectStylesRules = getInjectStyleRules(this.config.blockClass).concat(this.config.insertStyleRules);
for (var idx = 0; idx < injectStylesRules.length; idx++) {
styleEl.sheet.insertRule(injectStylesRules[idx], idx);
}
this.emitter.emit(ReplayerEvents.FullsnapshotRebuilded, event);
this.waitForStylesheetLoad();
};
Replayer.prototype.waitForStylesheetLoad = function () {
var _this = this;
var _a;
var head = (_a = this.iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.head;
if (head) {
var unloadSheets_1 = new Set();
var timer_1;
var beforeLoadState_1 = this.service.state;
var unsubscribe_1 = this.service.subscribe(function (state) {
beforeLoadState_1 = state;
}).unsubscribe;
head
.querySelectorAll('link[rel="stylesheet"]')
.forEach(function (css) {
if (!css.sheet) {
unloadSheets_1.add(css);
css.addEventListener('load', function () {
unloadSheets_1.delete(css);
if (unloadSheets_1.size === 0 && timer_1 !== -1) {
if (beforeLoadState_1.matches('playing')) {
_this.play(_this.getCurrentTime());
}
_this.emitter.emit(ReplayerEvents.LoadStylesheetEnd);
if (timer_1) {
window.clearTimeout(timer_1);
}
unsubscribe_1();
}
});
}
});
if (unloadSheets_1.size > 0) {
this.service.send({ type: 'PAUSE' });
this.emitter.emit(ReplayerEvents.LoadStylesheetStart);
timer_1 = window.setTimeout(function () {
if (beforeLoadState_1.matches('playing')) {
_this.play(_this.getCurrentTime());
}
timer_1 = -1;
unsubscribe_1();
}, this.config.loadTimeout);
}
}
};
Replayer.prototype.applyIncremental = function (e, isSync) {
var _this = this;
var d = e.data;
switch (d.source) {
case IncrementalSource.Mutation: {
if (isSync) {
d.adds.forEach(function (m) { return _this.treeIndex.add(m); });
d.texts.forEach(function (m) { return _this.treeIndex.text(m); });
d.attributes.forEach(function (m) { return _this.treeIndex.attribute(m); });
d.removes.forEach(function (m) { return _this.treeIndex.remove(m); });
}
this.applyMutation(d, isSync);
break;
}
case IncrementalSource.MouseMove:
if (isSync) {
var lastPosition = d.positions[d.positions.length - 1];
this.moveAndHover(d, lastPosition.x, lastPosition.y, lastPosition.id);
}
else {
d.positions.forEach(function (p) {
var action = {
doAction: function () {
_this.moveAndHover(d, p.x, p.y, p.id);
},
delay: p.timeOffset +
e.timestamp -
_this.service.state.context.baselineTime,
};
_this.timer.addAction(action);
});
}
break;
case IncrementalSource.MouseInteraction: {
if (d.id === -1) {
break;
}
var event = new Event(MouseInteractions[d.type].toLowerCase());
var target = mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
this.emitter.emit(ReplayerEvents.MouseInteraction, {
type: d.type,
target: target,
});
var triggerFocus = this.config.triggerFocus;
switch (d.type) {
case MouseInteractions.Blur:
if ('blur' in target) {
target.blur();
}
break;
case MouseInteractions.Focus:
if (triggerFocus && target.focus) {
target.focus({
preventScroll: true,
});
}
break;
case MouseInteractions.Click:
case MouseInteractions.TouchStart:
case MouseInteractions.TouchEnd:
if (!isSync) {
this.moveAndHover(d, d.x, d.y, d.id);
this.mouse.classList.remove('active');
void this.mouse.offsetWidth;
this.mouse.classList.add('active');
}
break;
default:
target.dispatchEvent(event);
}
break;
}
case IncrementalSource.Scroll: {
if (d.id === -1) {
break;
}
if (isSync) {
this.treeIndex.scroll(d);
break;
}
this.applyScroll(d);
break;
}
case IncrementalSource.ViewportResize:
this.emitter.emit(ReplayerEvents.Resize, {
width: d.width,
height: d.height,
});
break;
case IncrementalSource.Input: {
if (d.id === -1) {
break;
}
if (isSync) {
this.treeIndex.input(d);
break;
}
this.applyInput(d);
break;
}
case IncrementalSource.MediaInteraction: {
var target = mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
var mediaEl_1 = target;
if (d.type === MediaInteractions.Pause) {
mediaEl_1.pause();
}
if (d.type === MediaInteractions.Play) {
if (mediaEl_1.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
mediaEl_1.play();
}
else {
mediaEl_1.addEventListener('canplay', function () {
mediaEl_1.play();
});
}
}
break;
}
case IncrementalSource.StyleSheetRule: {
var target = mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
var styleEl = target;
var styleSheet_1 = styleEl.sheet;
if (d.adds) {
d.adds.forEach(function (_a) {
var rule = _a.rule, index = _a.index;
var _index = index === undefined
? undefined
: Math.min(index, styleSheet_1.rules.length);
try {
styleSheet_1.insertRule(rule, _index);
}
catch (e) {
}
});
}
if (d.removes) {
d.removes.forEach(function (_a) {
var index = _a.index;
styleSheet_1.deleteRule(index);
});
}
break;
}
}
};
Replayer.prototype.applyMutation = function (d, useVirtualParent) {
var _this = this;
d.removes.forEach(function (mutation) {
var target = mirror.getNode(mutation.id);
if (!target) {
return _this.warnNodeNotFound(d, mutation.id);
}
var parent = mirror.getNode(mutation.parentId);
if (!parent) {
return _this.warnNodeNotFound(d, mutation.parentId);
}
mirror.removeNodeFromMap(target);
if (parent) {
var realParent = _this.fragmentParentMap.get(parent);
if (realParent && realParent.contains(target)) {
realParent.removeChild(target);
}
else {
parent.removeChild(target);
}
}
});
var legacy_missingNodeMap = __assign({}, this.legacy_missingNodeRetryMap);
var queue = [];
var appendNode = function (mutation) {
if (!_this.iframe.contentDocument) {
return console.warn('Looks like your replayer has been destroyed.');
}
var parent = mirror.getNode(mutation.parentId);
if (!parent) {
return queue.push(mutation);
}
var parentInDocument = _this.iframe.contentDocument.contains(parent);
if (useVirtualParent && parentInDocument) {
var virtualParent = document.createDocumentFragment();
mirror.map[mutation.parentId] = virtualParent;
_this.fragmentParentMap.set(virtualParent, parent);
while (parent.firstChild) {
virtualParent.appendChild(parent.firstChild);
}
parent = virtualParent;
}
var previous = null;
var next = null;
if (mutation.previousId) {
previous = mirror.getNode(mutation.previousId);
}
if (mutation.nextId) {
next = mirror.getNode(mutation.nextId);
}
if (mutation.nextId !== null && mutation.nextId !== -1 && !next) {
return queue.push(mutation);
}
var target = buildNodeWithSN(mutation.node, _this.iframe.contentDocument, mirror.map, true);
if (mutation.previousId === -1 || mutation.nextId === -1) {
legacy_missingNodeMap[mutation.node.id] = {
node: target,
mutation: mutation,
};
return;
}
if (previous && previous.nextSibling && previous.nextSibling.parentNode) {
parent.insertBefore(target, previous.nextSibling);
}
else if (next && next.parentNode) {
parent.contains(next)
? parent.insertBefore(target, next)
: parent.insertBefore(target, null);
}
else {
parent.appendChild(target);
}
if (mutation.previousId || mutation.nextId) {
_this.legacy_resolveMissingNode(legacy_missingNodeMap, parent, target, mutation);
}
};
d.adds.forEach(function (mutation) {
appendNode(mutation);
});
while (queue.length) {
if (queue.every(function (m) { return !Boolean(mirror.getNode(m.parentId)); })) {
return queue.forEach(function (m) { return _this.warnNodeNotFound(d, m.node.id); });
}
var mutation = queue.shift();
appendNode(mutation);
}
if (Object.keys(legacy_missingNodeMap).length) {
Object.assign(this.legacy_missingNodeRetryMap, legacy_missingNodeMap);
}
d.texts.forEach(function (mutation) {
var target = mirror.getNode(mutation.id);
if (!target) {
return _this.warnNodeNotFound(d, mutation.id);
}
if (_this.fragmentParentMap.has(target)) {
target = _this.fragmentParentMap.get(target);
}
target.textContent = mutation.value;
});
d.attributes.forEach(function (mutation) {
var target = mirror.getNode(mutation.id);
if (!target) {
return _this.warnNodeNotFound(d, mutation.id);
}
if (_this.fragmentParentMap.has(target)) {
target = _this.fragmentParentMap.get(target);
}
for (var attributeName in mutation.attributes) {
if (typeof attributeName === 'string') {
var value = mutation.attributes[attributeName];
if (value !== null) {
target.setAttribute(attributeName, value);
}
else {
target.removeAttribute(attributeName);
}
}
}
});
};
Replayer.prototype.applyScroll = function (d) {
var target = mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
if (target === this.iframe.contentDocument) {
this.iframe.contentWindow.scrollTo({
top: d.y,
left: d.x,
behavior: 'smooth',
});
}
else {
try {
target.scrollTop = d.y;
target.scrollLeft = d.x;
}
catch (error) {
}
}
};
Replayer.prototype.applyInput = function (d) {
var target = mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
try {
target.checked = d.isChecked;
target.value = d.text;
}
catch (error) {
}
};
Replayer.prototype.legacy_resolveMissingNode = function (map, parent, target, targetMutation) {
var previousId = targetMutation.previousId, nextId = targetMutation.nextId;
var previousInMap = previousId && map[previousId];
var nextInMap = nextId && map[nextId];
if (previousInMap) {
var _a = previousInMap, node = _a.node, mutation = _a.mutation;
parent.insertBefore(node, target);
delete map[mutation.node.id];
delete this.legacy_missingNodeRetryMap[mutation.node.id];
if (mutation.previousId || mutation.nextId) {
this.legacy_resolveMissingNode(map, parent, node, mutation);
}
}
if (nextInMap) {
var _b = nextInMap, node = _b.node, mutation = _b.mutation;
parent.insertBefore(node, target.nextSibling);
delete map[mutation.node.id];
delete this.legacy_missingNodeRetryMap[mutation.node.id];
if (mutation.previousId || mutation.nextId) {
this.legacy_resolveMissingNode(map, parent, node, mutation);
}
}
};
Replayer.prototype.moveAndHover = function (d, x, y, id) {
this.mouse.style.left = x + "px";
this.mouse.style.top = y + "px";
var target = mirror.getNode(id);
if (!target) {
return this.debugNodeNotFound(d, id);
}
this.hoverElements(target);
};
Replayer.prototype.hoverElements = function (el) {
var _a;
(_a = this.iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.\\:hover').forEach(function (hoveredEl) {
hoveredEl.classList.remove(':hover');
});
var currentEl = el;
while (currentEl) {
if (currentEl.classList) {
currentEl.classList.add(':hover');
}
currentEl = currentEl.parentElement;
}
};
Replayer.prototype.isUserInteraction = function (event) {
if (event.type !== EventType.IncrementalSnapshot) {
return false;
}
return (event.data.source > IncrementalSource.Mutation &&
event.data.source <= IncrementalSource.Input);
};
Replayer.prototype.backToNormal = function () {
this.nextUserInteractionEvent = null;
if (this.speedService.state.matches('normal')) {
return;
}
this.speedService.send({ type: 'BACK_TO_NORMAL' });
this.emitter.emit(ReplayerEvents.SkipEnd, {
speed: this.speedService.state.context.normalSpeed,
});
};
Replayer.prototype.warnNodeNotFound = function (d, id) {
if (!this.config.showWarning) {
return;
}
console.warn(REPLAY_CONSOLE_PREFIX, "Node with id '" + id + "' not found in", d);
};
Replayer.prototype.debugNodeNotFound = function (d, id) {
if (!this.config.showDebug) {
return;
}
console.log(REPLAY_CONSOLE_PREFIX, "Node with id '" + id + "' not found in", d);
};
return Replayer;
}());
export { Replayer };