@openreplay/tracker 3.4.13 → 3.4.17-beta.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/cjs/app/context.d.ts +18 -0
- package/cjs/app/context.js +48 -0
- package/cjs/app/index.d.ts +11 -6
- package/cjs/app/index.js +33 -22
- package/cjs/app/observer/iframe_observer.d.ts +4 -0
- package/cjs/app/observer/iframe_observer.js +22 -0
- package/cjs/app/observer/observer.d.ts +25 -0
- package/cjs/app/{observer.js → observer/observer.js} +82 -170
- package/cjs/app/observer/shadow_root_observer.d.ts +4 -0
- package/cjs/app/observer/shadow_root_observer.js +21 -0
- package/cjs/app/observer/top_observer.d.ts +15 -0
- package/cjs/app/observer/top_observer.js +84 -0
- package/cjs/app/sanitizer.d.ts +16 -0
- package/cjs/app/sanitizer.js +46 -0
- package/cjs/index.d.ts +1 -0
- package/cjs/index.js +7 -1
- package/cjs/modules/exception.js +8 -1
- package/cjs/modules/img.js +15 -1
- package/cjs/modules/input.d.ts +3 -1
- package/cjs/modules/input.js +6 -3
- package/cjs/modules/mouse.js +1 -1
- package/lib/app/context.d.ts +18 -0
- package/lib/app/context.js +43 -0
- package/lib/app/index.d.ts +11 -6
- package/lib/app/index.js +33 -22
- package/lib/app/observer/iframe_observer.d.ts +4 -0
- package/lib/app/observer/iframe_observer.js +19 -0
- package/lib/app/observer/observer.d.ts +25 -0
- package/lib/app/{observer.js → observer/observer.js} +82 -170
- package/lib/app/observer/shadow_root_observer.d.ts +4 -0
- package/lib/app/observer/shadow_root_observer.js +18 -0
- package/lib/app/observer/top_observer.d.ts +15 -0
- package/lib/app/observer/top_observer.js +81 -0
- package/lib/app/sanitizer.d.ts +16 -0
- package/lib/app/sanitizer.js +44 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +7 -1
- package/lib/modules/exception.js +8 -1
- package/lib/modules/img.js +16 -2
- package/lib/modules/input.d.ts +3 -1
- package/lib/modules/input.js +6 -3
- package/lib/modules/mouse.js +1 -1
- package/package.json +1 -1
- package/cjs/app/observer.d.ts +0 -47
- package/lib/app/observer.d.ts +0 -47
|
@@ -1,40 +1,52 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { RemoveNodeAttribute, SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, SetNodeData, CreateTextNode, CreateElementNode, MoveNode, RemoveNode, } from "../../messages/index.js";
|
|
2
|
+
import { isInstance, inDocument } from "../context.js";
|
|
3
3
|
function isSVGElement(node) {
|
|
4
4
|
return node.namespaceURI === 'http://www.w3.org/2000/svg';
|
|
5
5
|
}
|
|
6
|
+
function isIgnored(node) {
|
|
7
|
+
if (isInstance(node, Text)) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (!isInstance(node, Element)) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
const tag = node.tagName.toUpperCase();
|
|
14
|
+
if (tag === 'LINK') {
|
|
15
|
+
const rel = node.getAttribute('rel');
|
|
16
|
+
const as = node.getAttribute('as');
|
|
17
|
+
return !((rel === null || rel === void 0 ? void 0 : rel.includes('stylesheet')) || as === "style" || as === "font");
|
|
18
|
+
}
|
|
19
|
+
return (tag === 'SCRIPT' ||
|
|
20
|
+
tag === 'NOSCRIPT' ||
|
|
21
|
+
tag === 'META' ||
|
|
22
|
+
tag === 'TITLE' ||
|
|
23
|
+
tag === 'BASE');
|
|
24
|
+
}
|
|
25
|
+
function isRootNode(node) {
|
|
26
|
+
return isInstance(node, Document) || isInstance(node, ShadowRoot);
|
|
27
|
+
}
|
|
28
|
+
function isObservable(node) {
|
|
29
|
+
if (isRootNode(node)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return !isIgnored(node);
|
|
33
|
+
}
|
|
6
34
|
export default class Observer {
|
|
7
|
-
constructor(app,
|
|
35
|
+
constructor(app, context = window) {
|
|
8
36
|
this.app = app;
|
|
9
|
-
this.options = options;
|
|
10
37
|
this.context = context;
|
|
11
|
-
this.
|
|
38
|
+
this.commited = [];
|
|
39
|
+
this.recents = [];
|
|
40
|
+
this.myNodes = [];
|
|
41
|
+
this.indexes = [];
|
|
42
|
+
this.attributesList = [];
|
|
43
|
+
this.textSet = new Set();
|
|
44
|
+
this.inUpperContext = context.parent === context; //TODO: get rid of context here
|
|
12
45
|
this.observer = new MutationObserver(this.app.safe((mutations) => {
|
|
13
|
-
var _a;
|
|
14
46
|
for (const mutation of mutations) {
|
|
15
47
|
const target = mutation.target;
|
|
16
48
|
const type = mutation.type;
|
|
17
|
-
|
|
18
|
-
// Document 'childList' might happen in case of iframe.
|
|
19
|
-
// TODO: generalize as much as possible
|
|
20
|
-
if (this.isInstance(target, Document)
|
|
21
|
-
&& type === 'childList'
|
|
22
|
-
//&& new Array(mutation.addedNodes).some(node => this.isInstance(node, HTMLHtmlElement))
|
|
23
|
-
) {
|
|
24
|
-
const parentFrame = (_a = target.defaultView) === null || _a === void 0 ? void 0 : _a.frameElement;
|
|
25
|
-
if (!parentFrame) {
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
this.bindTree(target.documentElement);
|
|
29
|
-
const frameID = this.app.nodes.getID(parentFrame);
|
|
30
|
-
const docID = this.app.nodes.getID(target.documentElement);
|
|
31
|
-
if (frameID === undefined || docID === undefined) {
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
this.app.send(CreateIFrameDocument(frameID, docID));
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (this.isIgnored(target) || !context.document.contains(target)) {
|
|
49
|
+
if (!isObservable(target) || !inDocument(target)) {
|
|
38
50
|
continue;
|
|
39
51
|
}
|
|
40
52
|
if (type === 'childList') {
|
|
@@ -50,7 +62,7 @@ export default class Observer {
|
|
|
50
62
|
if (id === undefined) {
|
|
51
63
|
continue;
|
|
52
64
|
}
|
|
53
|
-
if (id >= this.recents.length) {
|
|
65
|
+
if (id >= this.recents.length) { // TODO: something more convinient
|
|
54
66
|
this.recents[id] = undefined;
|
|
55
67
|
}
|
|
56
68
|
if (type === 'attributes') {
|
|
@@ -72,12 +84,6 @@ export default class Observer {
|
|
|
72
84
|
}
|
|
73
85
|
this.commitNodes();
|
|
74
86
|
}));
|
|
75
|
-
this.commited = [];
|
|
76
|
-
this.recents = [];
|
|
77
|
-
this.indexes = [0];
|
|
78
|
-
this.attributesList = [];
|
|
79
|
-
this.textSet = new Set();
|
|
80
|
-
this.textMasked = new Set();
|
|
81
87
|
}
|
|
82
88
|
clear() {
|
|
83
89
|
this.commited.length = 0;
|
|
@@ -85,40 +91,6 @@ export default class Observer {
|
|
|
85
91
|
this.indexes.length = 1;
|
|
86
92
|
this.attributesList.length = 0;
|
|
87
93
|
this.textSet.clear();
|
|
88
|
-
this.textMasked.clear();
|
|
89
|
-
}
|
|
90
|
-
// TODO: we need a type expert here so we won't have to ignore the lines
|
|
91
|
-
isInstance(node, constr) {
|
|
92
|
-
let context = this.context;
|
|
93
|
-
while (context.parent && context.parent !== context) {
|
|
94
|
-
// @ts-ignore
|
|
95
|
-
if (node instanceof context[constr.name]) {
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
// @ts-ignore
|
|
99
|
-
context = context.parent;
|
|
100
|
-
}
|
|
101
|
-
// @ts-ignore
|
|
102
|
-
return node instanceof context[constr.name];
|
|
103
|
-
}
|
|
104
|
-
isIgnored(node) {
|
|
105
|
-
if (this.isInstance(node, Text)) {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
if (!this.isInstance(node, Element)) {
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
const tag = node.tagName.toUpperCase();
|
|
112
|
-
if (tag === 'LINK') {
|
|
113
|
-
const rel = node.getAttribute('rel');
|
|
114
|
-
const as = node.getAttribute('as');
|
|
115
|
-
return !((rel === null || rel === void 0 ? void 0 : rel.includes('stylesheet')) || as === "style" || as === "font");
|
|
116
|
-
}
|
|
117
|
-
return (tag === 'SCRIPT' ||
|
|
118
|
-
tag === 'NOSCRIPT' ||
|
|
119
|
-
tag === 'META' ||
|
|
120
|
-
tag === 'TITLE' ||
|
|
121
|
-
tag === 'BASE');
|
|
122
94
|
}
|
|
123
95
|
sendNodeAttribute(id, node, name, value) {
|
|
124
96
|
if (isSVGElement(node)) {
|
|
@@ -148,7 +120,7 @@ export default class Observer {
|
|
|
148
120
|
return;
|
|
149
121
|
}
|
|
150
122
|
if (name === 'value' &&
|
|
151
|
-
|
|
123
|
+
isInstance(node, HTMLInputElement) &&
|
|
152
124
|
node.type !== 'button' &&
|
|
153
125
|
node.type !== 'reset' &&
|
|
154
126
|
node.type !== 'submit') {
|
|
@@ -158,7 +130,7 @@ export default class Observer {
|
|
|
158
130
|
this.app.send(new RemoveNodeAttribute(id, name));
|
|
159
131
|
return;
|
|
160
132
|
}
|
|
161
|
-
if (name === 'style' || name === 'href' &&
|
|
133
|
+
if (name === 'style' || name === 'href' && isInstance(node, HTMLLinkElement)) {
|
|
162
134
|
this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
|
|
163
135
|
return;
|
|
164
136
|
}
|
|
@@ -167,47 +139,27 @@ export default class Observer {
|
|
|
167
139
|
}
|
|
168
140
|
this.app.send(new SetNodeAttribute(id, name, value));
|
|
169
141
|
}
|
|
170
|
-
/* TODO: abstract sanitation */
|
|
171
|
-
getInnerTextSecure(el) {
|
|
172
|
-
const id = this.app.nodes.getID(el);
|
|
173
|
-
if (!id) {
|
|
174
|
-
return '';
|
|
175
|
-
}
|
|
176
|
-
return this.checkObscure(id, el.innerText);
|
|
177
|
-
}
|
|
178
|
-
checkObscure(id, data) {
|
|
179
|
-
if (this.textMasked.has(id)) {
|
|
180
|
-
return data.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█');
|
|
181
|
-
}
|
|
182
|
-
if (this.options.obscureTextNumbers) {
|
|
183
|
-
data = data.replace(/\d/g, '0');
|
|
184
|
-
}
|
|
185
|
-
if (this.options.obscureTextEmails) {
|
|
186
|
-
data = data.replace(/([^\s]+)@([^\s]+)\.([^\s]+)/g, (...f) => stars(f[1]) + '@' + stars(f[2]) + '.' + stars(f[3]));
|
|
187
|
-
}
|
|
188
|
-
return data;
|
|
189
|
-
}
|
|
190
142
|
sendNodeData(id, parentElement, data) {
|
|
191
|
-
if (
|
|
143
|
+
if (isInstance(parentElement, HTMLStyleElement) || isInstance(parentElement, SVGStyleElement)) {
|
|
192
144
|
this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref()));
|
|
193
145
|
return;
|
|
194
146
|
}
|
|
195
|
-
data = this.
|
|
147
|
+
data = this.app.sanitizer.sanitize(id, data);
|
|
196
148
|
this.app.send(new SetNodeData(id, data));
|
|
197
149
|
}
|
|
198
|
-
/* end TODO: abstract sanitation */
|
|
199
150
|
bindNode(node) {
|
|
200
151
|
const r = this.app.nodes.registerNode(node);
|
|
201
152
|
const id = r[0];
|
|
202
153
|
this.recents[id] = r[1] || this.recents[id] || false;
|
|
154
|
+
this.myNodes[id] = true;
|
|
203
155
|
}
|
|
204
156
|
bindTree(node) {
|
|
205
|
-
if (
|
|
157
|
+
if (!isObservable(node)) {
|
|
206
158
|
return;
|
|
207
159
|
}
|
|
208
160
|
this.bindNode(node);
|
|
209
161
|
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
|
|
210
|
-
acceptNode: (node) =>
|
|
162
|
+
acceptNode: (node) => isIgnored(node) || this.app.nodes.getID(node) !== undefined
|
|
211
163
|
? NodeFilter.FILTER_REJECT
|
|
212
164
|
: NodeFilter.FILTER_ACCEPT,
|
|
213
165
|
},
|
|
@@ -224,12 +176,15 @@ export default class Observer {
|
|
|
224
176
|
}
|
|
225
177
|
}
|
|
226
178
|
_commitNode(id, node) {
|
|
179
|
+
if (isRootNode(node)) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
227
182
|
const parent = node.parentNode;
|
|
228
183
|
let parentID;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
184
|
+
// Disable parent check for the upper context HTMLHtmlElement, because it is root there... (before)
|
|
185
|
+
// TODO: get rid of "special" cases (there is an issue with CreateDocument altered behaviour though)
|
|
186
|
+
// TODO: Clean the logic (though now it workd fine)
|
|
187
|
+
if (!isInstance(node, HTMLHtmlElement) || !this.inUpperContext) {
|
|
233
188
|
if (parent === null) {
|
|
234
189
|
this.unbindNode(node);
|
|
235
190
|
return false;
|
|
@@ -243,23 +198,20 @@ export default class Observer {
|
|
|
243
198
|
this.unbindNode(node);
|
|
244
199
|
return false;
|
|
245
200
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.indexes[id] = this.indexes[siblingID] + 1;
|
|
256
|
-
break;
|
|
257
|
-
}
|
|
258
|
-
sibling = sibling.previousSibling;
|
|
259
|
-
}
|
|
260
|
-
if (sibling === null) {
|
|
261
|
-
this.indexes[id] = 0;
|
|
201
|
+
this.app.sanitizer.handleNode(id, parentID, node);
|
|
202
|
+
}
|
|
203
|
+
let sibling = node.previousSibling;
|
|
204
|
+
while (sibling !== null) {
|
|
205
|
+
const siblingID = this.app.nodes.getID(sibling);
|
|
206
|
+
if (siblingID !== undefined) {
|
|
207
|
+
this.commitNode(siblingID);
|
|
208
|
+
this.indexes[id] = this.indexes[siblingID] + 1;
|
|
209
|
+
break;
|
|
262
210
|
}
|
|
211
|
+
sibling = sibling.previousSibling;
|
|
212
|
+
}
|
|
213
|
+
if (sibling === null) {
|
|
214
|
+
this.indexes[id] = 0; //
|
|
263
215
|
}
|
|
264
216
|
const isNew = this.recents[id];
|
|
265
217
|
const index = this.indexes[id];
|
|
@@ -267,7 +219,7 @@ export default class Observer {
|
|
|
267
219
|
throw 'commitNode: missing node index';
|
|
268
220
|
}
|
|
269
221
|
if (isNew === true) {
|
|
270
|
-
if (
|
|
222
|
+
if (isInstance(node, Element)) {
|
|
271
223
|
if (parentID !== undefined) {
|
|
272
224
|
this.app.send(new CreateElementNode(id, parentID, index, node.tagName, isSVGElement(node)));
|
|
273
225
|
}
|
|
@@ -275,12 +227,8 @@ export default class Observer {
|
|
|
275
227
|
const attr = node.attributes[i];
|
|
276
228
|
this.sendNodeAttribute(id, node, attr.nodeName, attr.value);
|
|
277
229
|
}
|
|
278
|
-
if (this.isInstance(node, HTMLIFrameElement) &&
|
|
279
|
-
(this.options.captureIFrames || node.getAttribute("data-openreplay-capture"))) {
|
|
280
|
-
this.handleIframe(node);
|
|
281
|
-
}
|
|
282
230
|
}
|
|
283
|
-
else if (
|
|
231
|
+
else if (isInstance(node, Text)) {
|
|
284
232
|
// for text node id != 0, hence parentID !== undefined and parent is Element
|
|
285
233
|
this.app.send(new CreateTextNode(id, parentID, index));
|
|
286
234
|
this.sendNodeData(id, parent, node.data);
|
|
@@ -292,7 +240,7 @@ export default class Observer {
|
|
|
292
240
|
}
|
|
293
241
|
const attr = this.attributesList[id];
|
|
294
242
|
if (attr !== undefined) {
|
|
295
|
-
if (!
|
|
243
|
+
if (!isInstance(node, Element)) {
|
|
296
244
|
throw 'commitNode: node is not an element';
|
|
297
245
|
}
|
|
298
246
|
for (const name of attr) {
|
|
@@ -300,7 +248,7 @@ export default class Observer {
|
|
|
300
248
|
}
|
|
301
249
|
}
|
|
302
250
|
if (this.textSet.has(id)) {
|
|
303
|
-
if (!
|
|
251
|
+
if (!isInstance(node, Text)) {
|
|
304
252
|
throw 'commitNode: node is not a text';
|
|
305
253
|
}
|
|
306
254
|
// for text node id != 0, hence parent is Element
|
|
@@ -322,6 +270,11 @@ export default class Observer {
|
|
|
322
270
|
commitNodes() {
|
|
323
271
|
let node;
|
|
324
272
|
for (let id = 0; id < this.recents.length; id++) {
|
|
273
|
+
// TODO: make things/logic nice here.
|
|
274
|
+
// commit required in any case if recents[id] true or false (in case of unbinding) or undefined (in case of attr change).
|
|
275
|
+
if (!this.myNodes[id]) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
325
278
|
this.commitNode(id);
|
|
326
279
|
if (this.recents[id] === true && (node = this.app.nodes.getNode(id))) {
|
|
327
280
|
this.app.nodes.callNodeCallbacks(node);
|
|
@@ -329,49 +282,9 @@ export default class Observer {
|
|
|
329
282
|
}
|
|
330
283
|
this.clear();
|
|
331
284
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const id = this.app.nodes.getID(iframe);
|
|
336
|
-
if (id === undefined) {
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
if (iframe.contentWindow === context) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
context = iframe.contentWindow;
|
|
343
|
-
if (!context) {
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
const observer = new Observer(this.app, this.options, context);
|
|
347
|
-
this.iframeObservers.push(observer);
|
|
348
|
-
observer.observeIframe(id, context);
|
|
349
|
-
});
|
|
350
|
-
this.app.attachEventListener(iframe, "load", handle);
|
|
351
|
-
handle();
|
|
352
|
-
}
|
|
353
|
-
// TODO: abstract common functionality, separate FrameObserver
|
|
354
|
-
observeIframe(id, context) {
|
|
355
|
-
const doc = context.document;
|
|
356
|
-
this.observer.observe(doc, {
|
|
357
|
-
childList: true,
|
|
358
|
-
attributes: true,
|
|
359
|
-
characterData: true,
|
|
360
|
-
subtree: true,
|
|
361
|
-
attributeOldValue: false,
|
|
362
|
-
characterDataOldValue: false,
|
|
363
|
-
});
|
|
364
|
-
this.bindTree(doc.documentElement);
|
|
365
|
-
const docID = this.app.nodes.getID(doc.documentElement);
|
|
366
|
-
if (docID === undefined) {
|
|
367
|
-
console.log("Wrong");
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
this.app.send(CreateIFrameDocument(id, docID));
|
|
371
|
-
this.commitNodes();
|
|
372
|
-
}
|
|
373
|
-
observe() {
|
|
374
|
-
this.observer.observe(this.context.document, {
|
|
285
|
+
// ISSSUE
|
|
286
|
+
observeRoot(node, beforeCommit, nodeToBind = node) {
|
|
287
|
+
this.observer.observe(node, {
|
|
375
288
|
childList: true,
|
|
376
289
|
attributes: true,
|
|
377
290
|
characterData: true,
|
|
@@ -379,14 +292,13 @@ export default class Observer {
|
|
|
379
292
|
attributeOldValue: false,
|
|
380
293
|
characterDataOldValue: false,
|
|
381
294
|
});
|
|
382
|
-
this.
|
|
383
|
-
|
|
295
|
+
this.bindTree(nodeToBind);
|
|
296
|
+
beforeCommit(this.app.nodes.getID(node));
|
|
384
297
|
this.commitNodes();
|
|
385
298
|
}
|
|
386
299
|
disconnect() {
|
|
387
|
-
this.iframeObservers.forEach(o => o.disconnect());
|
|
388
|
-
this.iframeObservers = [];
|
|
389
300
|
this.observer.disconnect();
|
|
390
301
|
this.clear();
|
|
302
|
+
this.myNodes.length = 0;
|
|
391
303
|
}
|
|
392
304
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Observer from "./observer.js";
|
|
2
|
+
import { CreateIFrameDocument } from "../../messages/index.js";
|
|
3
|
+
export default class ShadowRootObserver extends Observer {
|
|
4
|
+
observe(el) {
|
|
5
|
+
const shRoot = el.shadowRoot;
|
|
6
|
+
const hostID = this.app.nodes.getID(el);
|
|
7
|
+
if (!shRoot || hostID === undefined) {
|
|
8
|
+
return;
|
|
9
|
+
} // log
|
|
10
|
+
this.observeRoot(shRoot, (rootID) => {
|
|
11
|
+
if (rootID === undefined) {
|
|
12
|
+
console.log("OpenReplay: Shadow Root was not bound");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.app.send(CreateIFrameDocument(hostID, rootID));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Observer from "./observer.js";
|
|
2
|
+
import App from "../index.js";
|
|
3
|
+
export interface Options {
|
|
4
|
+
captureIFrames: boolean;
|
|
5
|
+
}
|
|
6
|
+
export default class TopObserver extends Observer {
|
|
7
|
+
private readonly options;
|
|
8
|
+
constructor(app: App, options: Partial<Options>);
|
|
9
|
+
private iframeObservers;
|
|
10
|
+
private handleIframe;
|
|
11
|
+
private shadowRootObservers;
|
|
12
|
+
private handleShadowRoot;
|
|
13
|
+
observe(): void;
|
|
14
|
+
disconnect(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import Observer from "./observer.js";
|
|
2
|
+
import { isInstance } from "../context.js";
|
|
3
|
+
import IFrameObserver from "./iframe_observer.js";
|
|
4
|
+
import ShadowRootObserver from "./shadow_root_observer.js";
|
|
5
|
+
import { CreateDocument } from "../../messages/index.js";
|
|
6
|
+
const attachShadowNativeFn = Element.prototype.attachShadow;
|
|
7
|
+
export default class TopObserver extends Observer {
|
|
8
|
+
constructor(app, options) {
|
|
9
|
+
super(app);
|
|
10
|
+
this.iframeObservers = [];
|
|
11
|
+
this.shadowRootObservers = [];
|
|
12
|
+
this.options = Object.assign({
|
|
13
|
+
captureIFrames: false
|
|
14
|
+
}, options);
|
|
15
|
+
// IFrames
|
|
16
|
+
this.app.nodes.attachNodeCallback(node => {
|
|
17
|
+
if (isInstance(node, HTMLIFrameElement) &&
|
|
18
|
+
(this.options.captureIFrames || node.getAttribute("data-openreplay-capture"))) {
|
|
19
|
+
this.handleIframe(node);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
// ShadowDOM
|
|
23
|
+
this.app.nodes.attachNodeCallback(node => {
|
|
24
|
+
if (isInstance(node, Element) && node.shadowRoot !== null) {
|
|
25
|
+
this.handleShadowRoot(node.shadowRoot);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
handleIframe(iframe) {
|
|
30
|
+
let context = null;
|
|
31
|
+
const handle = this.app.safe(() => {
|
|
32
|
+
const id = this.app.nodes.getID(iframe);
|
|
33
|
+
if (id === undefined) {
|
|
34
|
+
return;
|
|
35
|
+
} //log
|
|
36
|
+
if (iframe.contentWindow === context) {
|
|
37
|
+
return;
|
|
38
|
+
} //Does this happen frequently?
|
|
39
|
+
context = iframe.contentWindow;
|
|
40
|
+
if (!context) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const observer = new IFrameObserver(this.app, context);
|
|
44
|
+
this.iframeObservers.push(observer);
|
|
45
|
+
observer.observe(iframe);
|
|
46
|
+
});
|
|
47
|
+
this.app.attachEventListener(iframe, "load", handle);
|
|
48
|
+
handle();
|
|
49
|
+
}
|
|
50
|
+
handleShadowRoot(shRoot) {
|
|
51
|
+
const observer = new ShadowRootObserver(this.app, this.context);
|
|
52
|
+
this.shadowRootObservers.push(observer);
|
|
53
|
+
observer.observe(shRoot.host);
|
|
54
|
+
}
|
|
55
|
+
observe() {
|
|
56
|
+
// Protection from several subsequent calls?
|
|
57
|
+
const observer = this;
|
|
58
|
+
Element.prototype.attachShadow = function () {
|
|
59
|
+
const shadow = attachShadowNativeFn.apply(this, arguments);
|
|
60
|
+
observer.handleShadowRoot(shadow);
|
|
61
|
+
return shadow;
|
|
62
|
+
};
|
|
63
|
+
// Can observe documentElement (<html>) here, because it is not supposed to be changing.
|
|
64
|
+
// However, it is possible in some exotic cases and may cause an ignorance of the newly created <html>
|
|
65
|
+
// In this case context.document have to be observed, but this will cause
|
|
66
|
+
// the change in the re-player behaviour caused by CreateDocument message:
|
|
67
|
+
// the 0-node ("fRoot") will become #document rather than documentElement as it is now.
|
|
68
|
+
// Alternatively - observe(#document) then bindNode(documentElement)
|
|
69
|
+
this.observeRoot(this.context.document, () => {
|
|
70
|
+
this.app.send(new CreateDocument());
|
|
71
|
+
}, this.context.document.documentElement);
|
|
72
|
+
}
|
|
73
|
+
disconnect() {
|
|
74
|
+
Element.prototype.attachShadow = attachShadowNativeFn;
|
|
75
|
+
this.iframeObservers.forEach(o => o.disconnect());
|
|
76
|
+
this.iframeObservers = [];
|
|
77
|
+
this.shadowRootObservers.forEach(o => o.disconnect());
|
|
78
|
+
this.shadowRootObservers = [];
|
|
79
|
+
super.disconnect();
|
|
80
|
+
}
|
|
81
|
+
}
|
package/lib/app/sanitizer.d.ts
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import App from "./index.js";
|
|
2
|
+
export interface Options {
|
|
3
|
+
obscureTextEmails: boolean;
|
|
4
|
+
obscureTextNumbers: boolean;
|
|
5
|
+
}
|
|
6
|
+
export default class Sanitizer {
|
|
7
|
+
private readonly app;
|
|
8
|
+
private readonly masked;
|
|
9
|
+
private readonly options;
|
|
10
|
+
constructor(app: App, options: Partial<Options>);
|
|
11
|
+
handleNode(id: number, parentID: number, node: Node): void;
|
|
12
|
+
sanitize(id: number, data: string): string;
|
|
13
|
+
isMasked(id: number): boolean;
|
|
14
|
+
getInnerTextSecure(el: HTMLElement): string;
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
package/lib/app/sanitizer.js
CHANGED
|
@@ -1 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
import { stars, hasOpenreplayAttribute } from "../utils.js";
|
|
2
|
+
import { isInstance } from "./context.js";
|
|
3
|
+
export default class Sanitizer {
|
|
4
|
+
constructor(app, options) {
|
|
5
|
+
this.app = app;
|
|
6
|
+
this.masked = new Set();
|
|
7
|
+
this.options = Object.assign({
|
|
8
|
+
obscureTextEmails: true,
|
|
9
|
+
obscureTextNumbers: false,
|
|
10
|
+
}, options);
|
|
11
|
+
}
|
|
12
|
+
handleNode(id, parentID, node) {
|
|
13
|
+
if (this.masked.has(parentID) ||
|
|
14
|
+
(isInstance(node, Element) && hasOpenreplayAttribute(node, 'masked'))) {
|
|
15
|
+
this.masked.add(id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
sanitize(id, data) {
|
|
19
|
+
if (this.masked.has(id)) {
|
|
20
|
+
// TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
|
|
21
|
+
return data.trim().replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█');
|
|
22
|
+
}
|
|
23
|
+
if (this.options.obscureTextNumbers) {
|
|
24
|
+
data = data.replace(/\d/g, '0');
|
|
25
|
+
}
|
|
26
|
+
if (this.options.obscureTextEmails) {
|
|
27
|
+
data = data.replace(/([^\s]+)@([^\s]+)\.([^\s]+)/g, (...f) => stars(f[1]) + '@' + stars(f[2]) + '.' + stars(f[3]));
|
|
28
|
+
}
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
isMasked(id) {
|
|
32
|
+
return this.masked.has(id);
|
|
33
|
+
}
|
|
34
|
+
getInnerTextSecure(el) {
|
|
35
|
+
const id = this.app.nodes.getID(el);
|
|
36
|
+
if (!id) {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
return this.sanitize(id, el.innerText);
|
|
40
|
+
}
|
|
41
|
+
clear() {
|
|
42
|
+
this.masked.clear();
|
|
43
|
+
}
|
|
44
|
+
}
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -111,7 +111,7 @@ export default class API {
|
|
|
111
111
|
// no-cors issue only with text/plain or not-set Content-Type
|
|
112
112
|
// req.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
|
113
113
|
req.send(JSON.stringify({
|
|
114
|
-
trackerVersion: '3.4.
|
|
114
|
+
trackerVersion: '3.4.17-beta.0',
|
|
115
115
|
projectKey: options.projectKey,
|
|
116
116
|
doNotTrack,
|
|
117
117
|
// TODO: add precise reason (an exact API missing)
|
|
@@ -219,4 +219,10 @@ export default class API {
|
|
|
219
219
|
this.app.send(new CustomIssue(key, payload));
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
+
resetNextPageSession(flag) {
|
|
223
|
+
if (typeof flag !== 'boolean' || !this.app) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.app.resetNextPageSession(flag);
|
|
227
|
+
}
|
|
222
228
|
}
|
package/lib/modules/exception.js
CHANGED
|
@@ -37,7 +37,14 @@ export function getExceptionMessageFromEvent(e) {
|
|
|
37
37
|
return getExceptionMessage(e.reason, []);
|
|
38
38
|
}
|
|
39
39
|
else {
|
|
40
|
-
|
|
40
|
+
let message;
|
|
41
|
+
try {
|
|
42
|
+
message = JSON.stringify(e.reason);
|
|
43
|
+
}
|
|
44
|
+
catch (_) {
|
|
45
|
+
message = String(e.reason);
|
|
46
|
+
}
|
|
47
|
+
return new JSException('Unhandled Promise Rejection', message, '[]');
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
return null;
|
package/lib/modules/img.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { timestamp, isURL } from "../utils.js";
|
|
2
|
-
import { ResourceTiming, SetNodeAttributeURLBased } from "../messages/index.js";
|
|
2
|
+
import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from "../messages/index.js";
|
|
3
|
+
const PLACEHOLDER_SRC = "https://static.openreplay.com/tracker/placeholder.jpeg";
|
|
3
4
|
export default function (app) {
|
|
5
|
+
function sendPlaceholder(id, node) {
|
|
6
|
+
app.send(new SetNodeAttribute(id, "src", PLACEHOLDER_SRC));
|
|
7
|
+
const { width, height } = node.getBoundingClientRect();
|
|
8
|
+
if (!node.hasAttribute("width")) {
|
|
9
|
+
app.send(new SetNodeAttribute(id, "width", String(width)));
|
|
10
|
+
}
|
|
11
|
+
if (!node.hasAttribute("height")) {
|
|
12
|
+
app.send(new SetNodeAttribute(id, "height", String(height)));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
const sendImgSrc = app.safe(function () {
|
|
5
16
|
const id = app.nodes.getID(this);
|
|
6
17
|
if (id === undefined) {
|
|
@@ -15,7 +26,10 @@ export default function (app) {
|
|
|
15
26
|
app.send(new ResourceTiming(timestamp(), 0, 0, 0, 0, 0, src, 'img'));
|
|
16
27
|
}
|
|
17
28
|
}
|
|
18
|
-
else if (src.length
|
|
29
|
+
else if (src.length >= 1e5 || app.sanitizer.isMasked(id)) {
|
|
30
|
+
sendPlaceholder(id, this);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
19
33
|
app.send(new SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref()));
|
|
20
34
|
}
|
|
21
35
|
});
|
package/lib/modules/input.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import App from "../app/index.js";
|
|
2
|
-
|
|
2
|
+
declare type TextEditableElement = HTMLInputElement | HTMLTextAreaElement;
|
|
3
|
+
export declare function getInputLabel(node: TextEditableElement): string;
|
|
3
4
|
export declare const enum InputMode {
|
|
4
5
|
Plain = 0,
|
|
5
6
|
Obscured = 1,
|
|
@@ -11,3 +12,4 @@ export interface Options {
|
|
|
11
12
|
defaultInputMode: InputMode;
|
|
12
13
|
}
|
|
13
14
|
export default function (app: App, opts: Partial<Options>): void;
|
|
15
|
+
export {};
|