@openreplay/tracker 3.6.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +8 -0
- package/.prettierignore +1 -0
- package/LICENSE +1 -1
- package/cjs/app/guards.d.ts +2 -1
- package/cjs/app/guards.js +6 -3
- package/cjs/app/index.d.ts +28 -23
- package/cjs/app/index.js +101 -83
- package/cjs/app/logger.js +6 -3
- package/cjs/app/messages.d.ts +52 -0
- package/cjs/app/messages.gen.d.ts +57 -0
- package/cjs/app/messages.gen.js +493 -0
- package/cjs/app/messages.js +234 -0
- package/cjs/app/nodes.d.ts +1 -1
- package/cjs/app/nodes.js +2 -0
- package/cjs/app/observer/iframe_observer.d.ts +1 -1
- package/cjs/app/observer/iframe_observer.js +3 -3
- package/cjs/app/observer/observer.d.ts +2 -3
- package/cjs/app/observer/observer.js +50 -52
- package/cjs/app/observer/shadow_root_observer.d.ts +1 -1
- package/cjs/app/observer/shadow_root_observer.js +3 -3
- package/cjs/app/observer/top_observer.d.ts +13 -2
- package/cjs/app/observer/top_observer.js +58 -23
- package/cjs/app/sanitizer.d.ts +1 -1
- package/cjs/app/sanitizer.js +5 -5
- package/cjs/app/session.d.ts +20 -2
- package/cjs/app/session.js +65 -6
- package/cjs/app/ticker.d.ts +1 -1
- package/cjs/common/{webworker.d.ts → interaction.d.ts} +5 -5
- package/cjs/common/{types.js → interaction.js} +0 -0
- package/cjs/common/messages.gen.d.ts +382 -0
- package/cjs/common/{webworker.js → messages.gen.js} +1 -0
- package/cjs/index.d.ts +10 -9
- package/cjs/index.js +47 -36
- package/cjs/modules/adoptedStyleSheets.d.ts +2 -0
- package/cjs/modules/adoptedStyleSheets.js +127 -0
- package/cjs/modules/connection.d.ts +1 -1
- package/cjs/modules/connection.js +2 -2
- package/cjs/modules/console.d.ts +1 -1
- package/cjs/modules/console.js +7 -21
- package/cjs/modules/cssrules.d.ts +1 -1
- package/cjs/modules/cssrules.js +18 -14
- package/cjs/modules/exception.d.ts +3 -3
- package/cjs/modules/exception.js +23 -18
- package/cjs/modules/img.d.ts +1 -1
- package/cjs/modules/img.js +39 -26
- package/cjs/modules/input.d.ts +1 -1
- package/cjs/modules/input.js +21 -21
- package/cjs/modules/mouse.d.ts +1 -1
- package/cjs/modules/mouse.js +50 -43
- package/cjs/modules/performance.d.ts +1 -1
- package/cjs/modules/performance.js +2 -2
- package/cjs/modules/scroll.d.ts +1 -1
- package/cjs/modules/scroll.js +16 -7
- package/cjs/modules/timing.d.ts +1 -1
- package/cjs/modules/timing.js +14 -26
- package/cjs/modules/viewport.d.ts +1 -1
- package/cjs/modules/viewport.js +4 -4
- package/cjs/utils.js +7 -7
- package/cjs/vendors/finder/finder.js +53 -48
- package/lib/app/guards.d.ts +2 -1
- package/lib/app/guards.js +4 -2
- package/lib/app/index.d.ts +28 -23
- package/lib/app/index.js +109 -91
- package/lib/app/logger.js +6 -3
- package/lib/app/messages.d.ts +52 -0
- package/lib/app/messages.gen.d.ts +57 -0
- package/lib/app/messages.gen.js +434 -0
- package/lib/app/messages.js +181 -0
- package/lib/app/nodes.d.ts +1 -1
- package/lib/app/nodes.js +2 -0
- package/lib/app/observer/iframe_observer.d.ts +1 -1
- package/lib/app/observer/iframe_observer.js +3 -3
- package/lib/app/observer/observer.d.ts +2 -3
- package/lib/app/observer/observer.js +51 -53
- package/lib/app/observer/shadow_root_observer.d.ts +1 -1
- package/lib/app/observer/shadow_root_observer.js +3 -3
- package/lib/app/observer/top_observer.d.ts +13 -2
- package/lib/app/observer/top_observer.js +62 -27
- package/lib/app/sanitizer.d.ts +1 -1
- package/lib/app/sanitizer.js +7 -7
- package/lib/app/session.d.ts +20 -2
- package/lib/app/session.js +65 -6
- package/lib/app/ticker.d.ts +1 -1
- package/lib/common/{webworker.d.ts → interaction.d.ts} +5 -5
- package/lib/common/{types.js → interaction.js} +0 -0
- package/lib/common/messages.gen.d.ts +382 -0
- package/lib/common/messages.gen.js +2 -0
- package/lib/common/tsconfig.tsbuildinfo +1 -1
- package/lib/index.d.ts +10 -9
- package/lib/index.js +60 -49
- package/lib/modules/adoptedStyleSheets.d.ts +2 -0
- package/lib/modules/adoptedStyleSheets.js +124 -0
- package/lib/modules/connection.d.ts +1 -1
- package/lib/modules/connection.js +2 -2
- package/lib/modules/console.d.ts +1 -1
- package/lib/modules/console.js +8 -22
- package/lib/modules/cssrules.d.ts +1 -1
- package/lib/modules/cssrules.js +19 -15
- package/lib/modules/exception.d.ts +3 -3
- package/lib/modules/exception.js +23 -18
- package/lib/modules/img.d.ts +1 -1
- package/lib/modules/img.js +41 -28
- package/lib/modules/input.d.ts +1 -1
- package/lib/modules/input.js +23 -23
- package/lib/modules/mouse.d.ts +1 -1
- package/lib/modules/mouse.js +53 -46
- package/lib/modules/performance.d.ts +1 -1
- package/lib/modules/performance.js +3 -3
- package/lib/modules/scroll.d.ts +1 -1
- package/lib/modules/scroll.js +17 -8
- package/lib/modules/timing.d.ts +1 -1
- package/lib/modules/timing.js +16 -28
- package/lib/modules/viewport.d.ts +1 -1
- package/lib/modules/viewport.js +4 -4
- package/lib/utils.js +7 -7
- package/lib/vendors/finder/finder.js +53 -48
- package/package.json +27 -10
- package/cjs/common/messages.d.ts +0 -444
- package/cjs/common/messages.js +0 -794
- package/cjs/common/types.d.ts +0 -9
- package/cjs/modules/longtasks.d.ts +0 -2
- package/cjs/modules/longtasks.js +0 -26
- package/lib/common/messages.d.ts +0 -444
- package/lib/common/messages.js +0 -790
- package/lib/common/types.d.ts +0 -9
- package/lib/common/webworker.js +0 -1
- package/lib/modules/longtasks.d.ts +0 -2
- package/lib/modules/longtasks.js +0 -23
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { RemoveNodeAttribute, SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, SetNodeData, CreateTextNode, CreateElementNode, MoveNode, RemoveNode, } from
|
|
2
|
-
import { isRootNode, isTextNode, isElementNode, isSVGElement, hasTag
|
|
1
|
+
import { RemoveNodeAttribute, SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, SetNodeData, CreateTextNode, CreateElementNode, MoveNode, RemoveNode, } from '../messages.gen.js';
|
|
2
|
+
import { isRootNode, isTextNode, isElementNode, isSVGElement, hasTag } from '../guards.js';
|
|
3
3
|
function isIgnored(node) {
|
|
4
4
|
if (isTextNode(node)) {
|
|
5
5
|
return false;
|
|
@@ -11,13 +11,9 @@ function isIgnored(node) {
|
|
|
11
11
|
if (tag === 'LINK') {
|
|
12
12
|
const rel = node.getAttribute('rel');
|
|
13
13
|
const as = node.getAttribute('as');
|
|
14
|
-
return !((rel === null || rel === void 0 ? void 0 : rel.includes('stylesheet')) || as ===
|
|
14
|
+
return !((rel === null || rel === void 0 ? void 0 : rel.includes('stylesheet')) || as === 'style' || as === 'font');
|
|
15
15
|
}
|
|
16
|
-
return (tag === 'SCRIPT' ||
|
|
17
|
-
tag === 'NOSCRIPT' ||
|
|
18
|
-
tag === 'META' ||
|
|
19
|
-
tag === 'TITLE' ||
|
|
20
|
-
tag === 'BASE');
|
|
16
|
+
return (tag === 'SCRIPT' || tag === 'NOSCRIPT' || tag === 'META' || tag === 'TITLE' || tag === 'BASE');
|
|
21
17
|
}
|
|
22
18
|
function isObservable(node) {
|
|
23
19
|
if (isRootNode(node)) {
|
|
@@ -30,17 +26,11 @@ function isObservable(node) {
|
|
|
30
26
|
- fix unbinding logic + send all removals first (ensure sequence is correct)
|
|
31
27
|
- use document as a 0-node in the upper context (should be updated in player at first)
|
|
32
28
|
*/
|
|
33
|
-
/*
|
|
34
|
-
Nikita:
|
|
35
|
-
- rn we only send unbind event for parent (all child nodes will be cut in the live replay anyways)
|
|
36
|
-
to prevent sending 1k+ unbinds for child nodes and making replay file bigger than it should be
|
|
37
|
-
*/
|
|
38
29
|
var RecentsType;
|
|
39
30
|
(function (RecentsType) {
|
|
40
31
|
RecentsType[RecentsType["New"] = 0] = "New";
|
|
41
32
|
RecentsType[RecentsType["Removed"] = 1] = "Removed";
|
|
42
33
|
RecentsType[RecentsType["Changed"] = 2] = "Changed";
|
|
43
|
-
RecentsType[RecentsType["RemovedChild"] = 3] = "RemovedChild";
|
|
44
34
|
})(RecentsType || (RecentsType = {}));
|
|
45
35
|
export default class Observer {
|
|
46
36
|
constructor(app, isTopContext = false) {
|
|
@@ -52,7 +42,8 @@ export default class Observer {
|
|
|
52
42
|
this.attributesMap = new Map();
|
|
53
43
|
this.textSet = new Set();
|
|
54
44
|
this.observer = new MutationObserver(this.app.safe((mutations) => {
|
|
55
|
-
for (const mutation of mutations) {
|
|
45
|
+
for (const mutation of mutations) {
|
|
46
|
+
// mutations order is sequential
|
|
56
47
|
const target = mutation.target;
|
|
57
48
|
const type = mutation.type;
|
|
58
49
|
if (!isObservable(target)) {
|
|
@@ -60,7 +51,10 @@ export default class Observer {
|
|
|
60
51
|
}
|
|
61
52
|
if (type === 'childList') {
|
|
62
53
|
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
|
63
|
-
|
|
54
|
+
// Should be the same as bindTree(mutation.removedNodes[i]), but logic needs to be be untied
|
|
55
|
+
if (isObservable(mutation.removedNodes[i])) {
|
|
56
|
+
this.bindNode(mutation.removedNodes[i]);
|
|
57
|
+
}
|
|
64
58
|
}
|
|
65
59
|
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
66
60
|
this.bindTree(mutation.addedNodes[i]);
|
|
@@ -81,7 +75,7 @@ export default class Observer {
|
|
|
81
75
|
}
|
|
82
76
|
let attr = this.attributesMap.get(id);
|
|
83
77
|
if (attr === undefined) {
|
|
84
|
-
this.attributesMap.set(id, attr = new Set());
|
|
78
|
+
this.attributesMap.set(id, (attr = new Set()));
|
|
85
79
|
}
|
|
86
80
|
attr.add(name);
|
|
87
81
|
continue;
|
|
@@ -107,16 +101,16 @@ export default class Observer {
|
|
|
107
101
|
name = name.substr(6);
|
|
108
102
|
}
|
|
109
103
|
if (value === null) {
|
|
110
|
-
this.app.send(
|
|
104
|
+
this.app.send(RemoveNodeAttribute(id, name));
|
|
111
105
|
}
|
|
112
106
|
else if (name === 'href') {
|
|
113
107
|
if (value.length > 1e5) {
|
|
114
108
|
value = '';
|
|
115
109
|
}
|
|
116
|
-
this.app.send(
|
|
110
|
+
this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
|
|
117
111
|
}
|
|
118
112
|
else {
|
|
119
|
-
this.app.send(
|
|
113
|
+
this.app.send(SetNodeAttribute(id, name, value));
|
|
120
114
|
}
|
|
121
115
|
return;
|
|
122
116
|
}
|
|
@@ -129,72 +123,75 @@ export default class Observer {
|
|
|
129
123
|
return;
|
|
130
124
|
}
|
|
131
125
|
if (name === 'value' &&
|
|
132
|
-
hasTag(node,
|
|
126
|
+
hasTag(node, 'INPUT') &&
|
|
133
127
|
node.type !== 'button' &&
|
|
134
128
|
node.type !== 'reset' &&
|
|
135
129
|
node.type !== 'submit') {
|
|
136
130
|
return;
|
|
137
131
|
}
|
|
138
132
|
if (value === null) {
|
|
139
|
-
this.app.send(
|
|
133
|
+
this.app.send(RemoveNodeAttribute(id, name));
|
|
140
134
|
return;
|
|
141
135
|
}
|
|
142
|
-
if (name === 'style' || name === 'href' && hasTag(node,
|
|
143
|
-
this.app.send(
|
|
136
|
+
if (name === 'style' || (name === 'href' && hasTag(node, 'LINK'))) {
|
|
137
|
+
this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
|
|
144
138
|
return;
|
|
145
139
|
}
|
|
146
140
|
if (name === 'href' || value.length > 1e5) {
|
|
147
141
|
value = '';
|
|
148
142
|
}
|
|
149
|
-
this.app.send(
|
|
143
|
+
this.app.send(SetNodeAttribute(id, name, value));
|
|
150
144
|
}
|
|
151
145
|
sendNodeData(id, parentElement, data) {
|
|
152
|
-
if (hasTag(parentElement,
|
|
153
|
-
this.app.send(
|
|
146
|
+
if (hasTag(parentElement, 'STYLE') || hasTag(parentElement, 'style')) {
|
|
147
|
+
this.app.send(SetCSSDataURLBased(id, data, this.app.getBaseHref()));
|
|
154
148
|
return;
|
|
155
149
|
}
|
|
156
150
|
data = this.app.sanitizer.sanitize(id, data);
|
|
157
|
-
this.app.send(
|
|
151
|
+
this.app.send(SetNodeData(id, data));
|
|
158
152
|
}
|
|
159
153
|
bindNode(node) {
|
|
160
154
|
const [id, isNew] = this.app.nodes.registerNode(node);
|
|
161
155
|
if (isNew) {
|
|
162
156
|
this.recents.set(id, RecentsType.New);
|
|
163
157
|
}
|
|
164
|
-
else if (this.recents.get(id) !== RecentsType.New) {
|
|
158
|
+
else if (this.recents.get(id) !== RecentsType.New) {
|
|
165
159
|
this.recents.set(id, RecentsType.Removed);
|
|
166
160
|
}
|
|
167
161
|
}
|
|
168
|
-
|
|
169
|
-
const [id] = this.app.nodes.registerNode(node);
|
|
170
|
-
this.recents.set(id, RecentsType.RemovedChild);
|
|
171
|
-
}
|
|
172
|
-
bindTree(node, isChildUnbinding = false) {
|
|
162
|
+
bindTree(node) {
|
|
173
163
|
if (!isObservable(node)) {
|
|
174
164
|
return;
|
|
175
165
|
}
|
|
176
166
|
this.bindNode(node);
|
|
177
167
|
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
|
|
178
|
-
acceptNode: (node) => isIgnored(node)
|
|
179
|
-
|| (this.app.nodes.getID(node) !== undefined && !isChildUnbinding)
|
|
168
|
+
acceptNode: (node) => isIgnored(node) || this.app.nodes.getID(node) !== undefined
|
|
180
169
|
? NodeFilter.FILTER_REJECT
|
|
181
170
|
: NodeFilter.FILTER_ACCEPT,
|
|
182
171
|
},
|
|
183
172
|
// @ts-ignore
|
|
184
173
|
false);
|
|
185
174
|
while (walker.nextNode()) {
|
|
186
|
-
|
|
187
|
-
this.unbindChildNode(walker.currentNode);
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
this.bindNode(walker.currentNode);
|
|
191
|
-
}
|
|
175
|
+
this.bindNode(walker.currentNode);
|
|
192
176
|
}
|
|
193
177
|
}
|
|
194
|
-
|
|
178
|
+
unbindTree(node) {
|
|
195
179
|
const id = this.app.nodes.unregisterNode(node);
|
|
196
180
|
if (id !== undefined && this.recents.get(id) === RecentsType.Removed) {
|
|
197
|
-
|
|
181
|
+
// Sending RemoveNode only for parent to maintain
|
|
182
|
+
this.app.send(RemoveNode(id));
|
|
183
|
+
// Unregistering all the children in order to clear the memory
|
|
184
|
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
|
|
185
|
+
acceptNode: (node) => isIgnored(node) || this.app.nodes.getID(node) === undefined
|
|
186
|
+
? NodeFilter.FILTER_REJECT
|
|
187
|
+
: NodeFilter.FILTER_ACCEPT,
|
|
188
|
+
},
|
|
189
|
+
// @ts-ignore
|
|
190
|
+
false);
|
|
191
|
+
while (walker.nextNode()) {
|
|
192
|
+
this.app.nodes.unregisterNode(walker.currentNode);
|
|
193
|
+
}
|
|
194
|
+
// MBTODO: count and send RemovedNodesCount (for the page crash detection in heuristics)
|
|
198
195
|
}
|
|
199
196
|
}
|
|
200
197
|
// A top-consumption function on the infinite lists test. (~1% of performance resources)
|
|
@@ -207,20 +204,20 @@ export default class Observer {
|
|
|
207
204
|
// Disable parent check for the upper context HTMLHtmlElement, because it is root there... (before)
|
|
208
205
|
// TODO: get rid of "special" cases (there is an issue with CreateDocument altered behaviour though)
|
|
209
206
|
// TODO: Clean the logic (though now it workd fine)
|
|
210
|
-
if (!hasTag(node,
|
|
207
|
+
if (!hasTag(node, 'HTML') || !this.isTopContext) {
|
|
211
208
|
if (parent === null) {
|
|
212
209
|
// Sometimes one observation contains attribute mutations for the removimg node, which gets ignored here.
|
|
213
|
-
// That shouldn't affect the visual rendering ( should it? )
|
|
214
|
-
this.
|
|
210
|
+
// That shouldn't affect the visual rendering ( should it? maybe when transition applied? )
|
|
211
|
+
this.unbindTree(node);
|
|
215
212
|
return false;
|
|
216
213
|
}
|
|
217
214
|
parentID = this.app.nodes.getID(parent);
|
|
218
215
|
if (parentID === undefined) {
|
|
219
|
-
this.
|
|
216
|
+
this.unbindTree(node);
|
|
220
217
|
return false;
|
|
221
218
|
}
|
|
222
219
|
if (!this.commitNode(parentID)) {
|
|
223
|
-
this.
|
|
220
|
+
this.unbindTree(node);
|
|
224
221
|
return false;
|
|
225
222
|
}
|
|
226
223
|
this.app.sanitizer.handleNode(id, parentID, node);
|
|
@@ -259,7 +256,7 @@ export default class Observer {
|
|
|
259
256
|
el.style.width = width + 'px';
|
|
260
257
|
el.style.height = height + 'px';
|
|
261
258
|
}
|
|
262
|
-
this.app.send(
|
|
259
|
+
this.app.send(CreateElementNode(id, parentID, index, el.tagName, isSVGElement(node)));
|
|
263
260
|
}
|
|
264
261
|
for (let i = 0; i < el.attributes.length; i++) {
|
|
265
262
|
const attr = el.attributes[i];
|
|
@@ -268,13 +265,13 @@ export default class Observer {
|
|
|
268
265
|
}
|
|
269
266
|
else if (isTextNode(node)) {
|
|
270
267
|
// for text node id != 0, hence parentID !== undefined and parent is Element
|
|
271
|
-
this.app.send(
|
|
268
|
+
this.app.send(CreateTextNode(id, parentID, index));
|
|
272
269
|
this.sendNodeData(id, parent, node.data);
|
|
273
270
|
}
|
|
274
271
|
return true;
|
|
275
272
|
}
|
|
276
273
|
if (recentsType === RecentsType.Removed && parentID !== undefined) {
|
|
277
|
-
this.app.send(
|
|
274
|
+
this.app.send(MoveNode(id, parentID, index));
|
|
278
275
|
}
|
|
279
276
|
const attr = this.attributesMap.get(id);
|
|
280
277
|
if (attr !== undefined) {
|
|
@@ -315,7 +312,8 @@ export default class Observer {
|
|
|
315
312
|
});
|
|
316
313
|
this.clear();
|
|
317
314
|
}
|
|
318
|
-
// ISSSUE
|
|
315
|
+
// ISSSUE (nodeToBinde should be the same as node. Look at the comment about 0-node at the beginning of the file.)
|
|
316
|
+
// TODO: use one observer instance for all iframes/shadowRoots (composition instiad of inheritance)
|
|
319
317
|
observeRoot(node, beforeCommit, nodeToBind = node) {
|
|
320
318
|
this.observer.observe(node, {
|
|
321
319
|
childList: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import Observer from
|
|
2
|
-
import { CreateIFrameDocument } from
|
|
1
|
+
import Observer from './observer.js';
|
|
2
|
+
import { CreateIFrameDocument } from '../messages.gen.js';
|
|
3
3
|
export default class ShadowRootObserver extends Observer {
|
|
4
4
|
observe(el) {
|
|
5
5
|
const shRoot = el.shadowRoot;
|
|
@@ -9,7 +9,7 @@ export default class ShadowRootObserver extends Observer {
|
|
|
9
9
|
} // log
|
|
10
10
|
this.observeRoot(shRoot, (rootID) => {
|
|
11
11
|
if (rootID === undefined) {
|
|
12
|
-
console.log(
|
|
12
|
+
console.log('OpenReplay: Shadow Root was not bound');
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
this.app.send(CreateIFrameDocument(hostID, rootID));
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import Observer from
|
|
2
|
-
import App from
|
|
1
|
+
import Observer from './observer.js';
|
|
2
|
+
import App from '../index.js';
|
|
3
3
|
export interface Options {
|
|
4
4
|
captureIFrames: boolean;
|
|
5
5
|
}
|
|
6
|
+
declare type Context = Window & typeof globalThis;
|
|
7
|
+
declare type ContextCallback = (context: Context) => void;
|
|
8
|
+
declare type Offset = {
|
|
9
|
+
top: number;
|
|
10
|
+
left: number;
|
|
11
|
+
};
|
|
6
12
|
export default class TopObserver extends Observer {
|
|
7
13
|
private readonly options;
|
|
8
14
|
constructor(app: App, options: Partial<Options>);
|
|
15
|
+
private readonly contextCallbacks;
|
|
16
|
+
private readonly contextsSet;
|
|
17
|
+
attachContextCallback(cb: ContextCallback): void;
|
|
18
|
+
getDocumentOffset(doc: Document): Offset;
|
|
9
19
|
private iframeObservers;
|
|
10
20
|
private handleIframe;
|
|
11
21
|
private shadowRootObservers;
|
|
@@ -13,3 +23,4 @@ export default class TopObserver extends Observer {
|
|
|
13
23
|
observe(): void;
|
|
14
24
|
disconnect(): void;
|
|
15
25
|
}
|
|
26
|
+
export {};
|
|
@@ -1,52 +1,86 @@
|
|
|
1
|
-
import Observer from
|
|
2
|
-
import { isElementNode, hasTag
|
|
3
|
-
import IFrameObserver from
|
|
4
|
-
import ShadowRootObserver from
|
|
5
|
-
import { CreateDocument } from
|
|
1
|
+
import Observer from './observer.js';
|
|
2
|
+
import { isElementNode, hasTag } from '../guards.js';
|
|
3
|
+
import IFrameObserver from './iframe_observer.js';
|
|
4
|
+
import ShadowRootObserver from './shadow_root_observer.js';
|
|
5
|
+
import { CreateDocument } from '../messages.gen.js';
|
|
6
6
|
import { IN_BROWSER, hasOpenreplayAttribute } from '../../utils.js';
|
|
7
|
+
function isPatchedDocument(doc) {
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
return typeof doc.__openreplay__getOffset === 'function';
|
|
10
|
+
}
|
|
7
11
|
const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () => new ShadowRoot();
|
|
8
12
|
export default class TopObserver extends Observer {
|
|
9
13
|
constructor(app, options) {
|
|
10
14
|
super(app, true);
|
|
15
|
+
this.contextCallbacks = [];
|
|
16
|
+
// Attached once per Tracker instance
|
|
17
|
+
this.contextsSet = new Set();
|
|
11
18
|
this.iframeObservers = [];
|
|
12
19
|
this.shadowRootObservers = [];
|
|
13
20
|
this.options = Object.assign({
|
|
14
|
-
captureIFrames: true
|
|
21
|
+
captureIFrames: true,
|
|
15
22
|
}, options);
|
|
16
23
|
// IFrames
|
|
17
|
-
this.app.nodes.attachNodeCallback(node => {
|
|
18
|
-
if (hasTag(node,
|
|
19
|
-
((this.options.captureIFrames && !hasOpenreplayAttribute(node,
|
|
20
|
-
|
|
24
|
+
this.app.nodes.attachNodeCallback((node) => {
|
|
25
|
+
if (hasTag(node, 'IFRAME') &&
|
|
26
|
+
((this.options.captureIFrames && !hasOpenreplayAttribute(node, 'obscured')) ||
|
|
27
|
+
hasOpenreplayAttribute(node, 'capture'))) {
|
|
21
28
|
this.handleIframe(node);
|
|
22
29
|
}
|
|
23
30
|
});
|
|
24
31
|
// ShadowDOM
|
|
25
|
-
this.app.nodes.attachNodeCallback(node => {
|
|
32
|
+
this.app.nodes.attachNodeCallback((node) => {
|
|
26
33
|
if (isElementNode(node) && node.shadowRoot !== null) {
|
|
27
34
|
this.handleShadowRoot(node.shadowRoot);
|
|
28
35
|
}
|
|
29
36
|
});
|
|
30
37
|
}
|
|
38
|
+
attachContextCallback(cb) {
|
|
39
|
+
this.contextCallbacks.push(cb);
|
|
40
|
+
}
|
|
41
|
+
// Le truc
|
|
42
|
+
getDocumentOffset(doc) {
|
|
43
|
+
if (isPatchedDocument(doc)) {
|
|
44
|
+
return doc.__openreplay__getOffset();
|
|
45
|
+
}
|
|
46
|
+
return { top: 0, left: 0 };
|
|
47
|
+
}
|
|
31
48
|
handleIframe(iframe) {
|
|
32
49
|
let doc = null;
|
|
50
|
+
let win = null;
|
|
33
51
|
const handle = this.app.safe(() => {
|
|
34
52
|
const id = this.app.nodes.getID(iframe);
|
|
35
53
|
if (id === undefined) {
|
|
54
|
+
//log
|
|
36
55
|
return;
|
|
37
|
-
} //log
|
|
38
|
-
if (iframe.contentDocument === doc) {
|
|
39
|
-
return;
|
|
40
|
-
} // How frequently can it happen?
|
|
41
|
-
doc = iframe.contentDocument;
|
|
42
|
-
if (!doc || !iframe.contentWindow) {
|
|
43
|
-
return;
|
|
44
56
|
}
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
const currentWin = iframe.contentWindow;
|
|
58
|
+
const currentDoc = iframe.contentDocument;
|
|
59
|
+
if (currentDoc && currentDoc !== doc) {
|
|
60
|
+
const observer = new IFrameObserver(this.app);
|
|
61
|
+
this.iframeObservers.push(observer);
|
|
62
|
+
observer.observe(iframe);
|
|
63
|
+
doc = currentDoc;
|
|
64
|
+
doc.__openreplay__getOffset = () => {
|
|
65
|
+
const { top, left } = this.getDocumentOffset(iframe.ownerDocument);
|
|
66
|
+
return {
|
|
67
|
+
top: iframe.offsetTop + top,
|
|
68
|
+
left: iframe.offsetLeft + left,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (currentWin &&
|
|
73
|
+
// Sometimes currentWin.window is null (not in specification). Such window object is not functional
|
|
74
|
+
currentWin === currentWin.window &&
|
|
75
|
+
!this.contextsSet.has(currentWin) // for each context callbacks called once per Tracker (TopObserver) instance
|
|
76
|
+
) {
|
|
77
|
+
this.contextsSet.add(currentWin);
|
|
78
|
+
//@ts-ignore https://github.com/microsoft/TypeScript/issues/41684
|
|
79
|
+
this.contextCallbacks.forEach((cb) => cb(currentWin));
|
|
80
|
+
win = currentWin;
|
|
81
|
+
}
|
|
48
82
|
});
|
|
49
|
-
iframe.addEventListener(
|
|
83
|
+
iframe.addEventListener('load', handle); // why app.attachEventListener not working?
|
|
50
84
|
handle();
|
|
51
85
|
}
|
|
52
86
|
handleShadowRoot(shRoot) {
|
|
@@ -58,25 +92,26 @@ export default class TopObserver extends Observer {
|
|
|
58
92
|
// Protection from several subsequent calls?
|
|
59
93
|
const observer = this;
|
|
60
94
|
Element.prototype.attachShadow = function () {
|
|
95
|
+
// eslint-disable-next-line
|
|
61
96
|
const shadow = attachShadowNativeFn.apply(this, arguments);
|
|
62
97
|
observer.handleShadowRoot(shadow);
|
|
63
98
|
return shadow;
|
|
64
99
|
};
|
|
65
100
|
// Can observe documentElement (<html>) here, because it is not supposed to be changing.
|
|
66
101
|
// However, it is possible in some exotic cases and may cause an ignorance of the newly created <html>
|
|
67
|
-
// In this case context.document have to be observed, but this will cause
|
|
68
|
-
// the change in the re-player behaviour caused by CreateDocument message:
|
|
102
|
+
// In this case context.document have to be observed, but this will cause
|
|
103
|
+
// the change in the re-player behaviour caused by CreateDocument message:
|
|
69
104
|
// the 0-node ("fRoot") will become #document rather than documentElement as it is now.
|
|
70
105
|
// Alternatively - observe(#document) then bindNode(documentElement)
|
|
71
106
|
this.observeRoot(window.document, () => {
|
|
72
|
-
this.app.send(
|
|
107
|
+
this.app.send(CreateDocument());
|
|
73
108
|
}, window.document.documentElement);
|
|
74
109
|
}
|
|
75
110
|
disconnect() {
|
|
76
111
|
Element.prototype.attachShadow = attachShadowNativeFn;
|
|
77
|
-
this.iframeObservers.forEach(o => o.disconnect());
|
|
112
|
+
this.iframeObservers.forEach((o) => o.disconnect());
|
|
78
113
|
this.iframeObservers = [];
|
|
79
|
-
this.shadowRootObservers.forEach(o => o.disconnect());
|
|
114
|
+
this.shadowRootObservers.forEach((o) => o.disconnect());
|
|
80
115
|
this.shadowRootObservers = [];
|
|
81
116
|
super.disconnect();
|
|
82
117
|
}
|
package/lib/app/sanitizer.d.ts
CHANGED
package/lib/app/sanitizer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { stars, hasOpenreplayAttribute } from
|
|
2
|
-
import { isElementNode } from
|
|
1
|
+
import { stars, hasOpenreplayAttribute } from '../utils.js';
|
|
2
|
+
import { isElementNode } from './guards.js';
|
|
3
3
|
export default class Sanitizer {
|
|
4
4
|
constructor(app, options) {
|
|
5
5
|
this.app = app;
|
|
@@ -12,20 +12,20 @@ export default class Sanitizer {
|
|
|
12
12
|
}
|
|
13
13
|
handleNode(id, parentID, node) {
|
|
14
14
|
if (this.masked.has(parentID) ||
|
|
15
|
-
(isElementNode(node) &&
|
|
16
|
-
hasOpenreplayAttribute(node, 'masked'))) {
|
|
15
|
+
(isElementNode(node) && hasOpenreplayAttribute(node, 'masked'))) {
|
|
17
16
|
this.masked.add(id);
|
|
18
17
|
}
|
|
19
18
|
if (this.maskedContainers.has(parentID) ||
|
|
20
|
-
(isElementNode(node) &&
|
|
21
|
-
hasOpenreplayAttribute(node, 'htmlmasked'))) {
|
|
19
|
+
(isElementNode(node) && hasOpenreplayAttribute(node, 'htmlmasked'))) {
|
|
22
20
|
this.maskedContainers.add(id);
|
|
23
21
|
}
|
|
24
22
|
}
|
|
25
23
|
sanitize(id, data) {
|
|
26
24
|
if (this.masked.has(id)) {
|
|
27
25
|
// TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
|
|
28
|
-
return data
|
|
26
|
+
return data
|
|
27
|
+
.trim()
|
|
28
|
+
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█');
|
|
29
29
|
}
|
|
30
30
|
if (this.options.obscureTextNumbers) {
|
|
31
31
|
data = data.replace(/\d/g, '0');
|
package/lib/app/session.d.ts
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
|
+
import type App from './index.js';
|
|
1
2
|
interface SessionInfo {
|
|
2
|
-
sessionID: string |
|
|
3
|
+
sessionID: string | undefined;
|
|
3
4
|
metadata: Record<string, string>;
|
|
4
5
|
userID: string | null;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
projectID?: string;
|
|
5
8
|
}
|
|
6
9
|
declare type OnUpdateCallback = (i: Partial<SessionInfo>) => void;
|
|
10
|
+
export declare type Options = {
|
|
11
|
+
session_token_key: string;
|
|
12
|
+
session_pageno_key: string;
|
|
13
|
+
};
|
|
7
14
|
export default class Session {
|
|
15
|
+
private readonly app;
|
|
16
|
+
private readonly options;
|
|
8
17
|
private metadata;
|
|
9
18
|
private userID;
|
|
10
19
|
private sessionID;
|
|
11
|
-
private callbacks;
|
|
20
|
+
private readonly callbacks;
|
|
21
|
+
private timestamp;
|
|
22
|
+
private projectID;
|
|
23
|
+
constructor(app: App, options: Options);
|
|
12
24
|
attachUpdateCallback(cb: OnUpdateCallback): void;
|
|
13
25
|
private handleUpdate;
|
|
14
26
|
update(newInfo: Partial<SessionInfo>): void;
|
|
15
27
|
setMetadata(key: string, value: string): void;
|
|
16
28
|
setUserID(userID: string): void;
|
|
29
|
+
private getPageNumber;
|
|
30
|
+
incPageNo(): number;
|
|
31
|
+
getSessionToken(): string | undefined;
|
|
32
|
+
setSessionToken(token: string): void;
|
|
33
|
+
applySessionHash(hash: string): void;
|
|
34
|
+
getSessionHash(): string | undefined;
|
|
17
35
|
getInfo(): SessionInfo;
|
|
18
36
|
reset(): void;
|
|
19
37
|
}
|
package/lib/app/session.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export default class Session {
|
|
2
|
-
constructor() {
|
|
2
|
+
constructor(app, options) {
|
|
3
|
+
this.app = app;
|
|
4
|
+
this.options = options;
|
|
3
5
|
this.metadata = {};
|
|
4
6
|
this.userID = null;
|
|
5
|
-
this.sessionID = null;
|
|
6
7
|
this.callbacks = [];
|
|
8
|
+
this.timestamp = 0;
|
|
7
9
|
}
|
|
8
10
|
attachUpdateCallback(cb) {
|
|
9
11
|
this.callbacks.push(cb);
|
|
@@ -15,18 +17,25 @@ export default class Session {
|
|
|
15
17
|
if (newInfo.sessionID == null) {
|
|
16
18
|
delete newInfo.sessionID;
|
|
17
19
|
}
|
|
18
|
-
this.callbacks.forEach(cb => cb(newInfo));
|
|
20
|
+
this.callbacks.forEach((cb) => cb(newInfo));
|
|
19
21
|
}
|
|
20
22
|
update(newInfo) {
|
|
21
|
-
if (newInfo.userID !== undefined) {
|
|
23
|
+
if (newInfo.userID !== undefined) {
|
|
24
|
+
// TODO clear nullable/undefinable types
|
|
22
25
|
this.userID = newInfo.userID;
|
|
23
26
|
}
|
|
24
27
|
if (newInfo.metadata !== undefined) {
|
|
25
|
-
Object.entries(newInfo.metadata).forEach(([k, v]) => this.metadata[k] = v);
|
|
28
|
+
Object.entries(newInfo.metadata).forEach(([k, v]) => (this.metadata[k] = v));
|
|
26
29
|
}
|
|
27
30
|
if (newInfo.sessionID !== undefined) {
|
|
28
31
|
this.sessionID = newInfo.sessionID;
|
|
29
32
|
}
|
|
33
|
+
if (newInfo.timestamp !== undefined) {
|
|
34
|
+
this.timestamp = newInfo.timestamp;
|
|
35
|
+
}
|
|
36
|
+
if (newInfo.projectID !== undefined) {
|
|
37
|
+
this.projectID = newInfo.projectID;
|
|
38
|
+
}
|
|
30
39
|
this.handleUpdate(newInfo);
|
|
31
40
|
}
|
|
32
41
|
setMetadata(key, value) {
|
|
@@ -37,16 +46,66 @@ export default class Session {
|
|
|
37
46
|
this.userID = userID;
|
|
38
47
|
this.handleUpdate({ userID });
|
|
39
48
|
}
|
|
49
|
+
getPageNumber() {
|
|
50
|
+
const pageNoStr = this.app.sessionStorage.getItem(this.options.session_pageno_key);
|
|
51
|
+
if (pageNoStr == null) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return parseInt(pageNoStr);
|
|
55
|
+
}
|
|
56
|
+
incPageNo() {
|
|
57
|
+
let pageNo = this.getPageNumber();
|
|
58
|
+
if (pageNo === undefined) {
|
|
59
|
+
pageNo = 0;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
pageNo++;
|
|
63
|
+
}
|
|
64
|
+
this.app.sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString());
|
|
65
|
+
return pageNo;
|
|
66
|
+
}
|
|
67
|
+
getSessionToken() {
|
|
68
|
+
return this.app.sessionStorage.getItem(this.options.session_token_key) || undefined;
|
|
69
|
+
}
|
|
70
|
+
setSessionToken(token) {
|
|
71
|
+
this.app.sessionStorage.setItem(this.options.session_token_key, token);
|
|
72
|
+
}
|
|
73
|
+
applySessionHash(hash) {
|
|
74
|
+
const hashParts = decodeURI(hash).split('&');
|
|
75
|
+
let token = hash;
|
|
76
|
+
let pageNoStr = '100500'; // back-compat for sessionToken
|
|
77
|
+
if (hashParts.length == 2) {
|
|
78
|
+
;
|
|
79
|
+
[token, pageNoStr] = hashParts;
|
|
80
|
+
}
|
|
81
|
+
if (!pageNoStr || !token) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.app.sessionStorage.setItem(this.options.session_token_key, token);
|
|
85
|
+
this.app.sessionStorage.setItem(this.options.session_pageno_key, pageNoStr);
|
|
86
|
+
}
|
|
87
|
+
getSessionHash() {
|
|
88
|
+
const pageNo = this.getPageNumber();
|
|
89
|
+
const token = this.getSessionToken();
|
|
90
|
+
if (pageNo === undefined || token === undefined) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
return encodeURI(String(pageNo) + '&' + token);
|
|
94
|
+
}
|
|
40
95
|
getInfo() {
|
|
41
96
|
return {
|
|
42
97
|
sessionID: this.sessionID,
|
|
43
98
|
metadata: this.metadata,
|
|
44
99
|
userID: this.userID,
|
|
100
|
+
timestamp: this.timestamp,
|
|
101
|
+
projectID: this.projectID,
|
|
45
102
|
};
|
|
46
103
|
}
|
|
47
104
|
reset() {
|
|
105
|
+
this.app.sessionStorage.removeItem(this.options.session_token_key);
|
|
48
106
|
this.metadata = {};
|
|
49
107
|
this.userID = null;
|
|
50
|
-
this.sessionID =
|
|
108
|
+
this.sessionID = undefined;
|
|
109
|
+
this.timestamp = 0;
|
|
51
110
|
}
|
|
52
111
|
}
|
package/lib/app/ticker.d.ts
CHANGED