@openreplay/tracker 3.4.11 → 3.4.15
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/index.d.ts +2 -2
- package/cjs/app/index.js +17 -5
- 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 +50 -0
- package/cjs/app/observer/observer.js +381 -0
- 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 +82 -0
- package/cjs/index.js +1 -1
- package/lib/app/index.d.ts +2 -2
- package/lib/app/index.js +16 -4
- 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 +50 -0
- package/lib/app/observer/observer.js +376 -0
- 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 +79 -0
- package/lib/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { stars, hasOpenreplayAttribute } from "../../utils.js";
|
|
2
|
+
import { RemoveNodeAttribute, SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, SetNodeData, CreateTextNode, CreateElementNode, MoveNode, RemoveNode, } from "../../messages/index.js";
|
|
3
|
+
function isSVGElement(node) {
|
|
4
|
+
return node.namespaceURI === 'http://www.w3.org/2000/svg';
|
|
5
|
+
}
|
|
6
|
+
// TODO: we need a type expert here so we won't have to ignore the lines
|
|
7
|
+
// TODO: use it everywhere (static function; export from which file? <-- global Window typing required)
|
|
8
|
+
export function isInstance(node, constr) {
|
|
9
|
+
const doc = node.ownerDocument;
|
|
10
|
+
if (!doc) { // null if Document
|
|
11
|
+
return constr.name === 'Document';
|
|
12
|
+
}
|
|
13
|
+
let context =
|
|
14
|
+
// @ts-ignore (for EI, Safary)
|
|
15
|
+
doc.parentWindow ||
|
|
16
|
+
doc.defaultView; // TODO: smart global typing for Window object
|
|
17
|
+
while (context.parent && context.parent !== context) {
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
if (node instanceof context[constr.name]) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
context = context.parent;
|
|
24
|
+
}
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
return node instanceof context[constr.name];
|
|
27
|
+
}
|
|
28
|
+
function isIgnored(node) {
|
|
29
|
+
if (isInstance(node, Text)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!isInstance(node, Element)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const tag = node.tagName.toUpperCase();
|
|
36
|
+
if (tag === 'LINK') {
|
|
37
|
+
const rel = node.getAttribute('rel');
|
|
38
|
+
const as = node.getAttribute('as');
|
|
39
|
+
return !((rel === null || rel === void 0 ? void 0 : rel.includes('stylesheet')) || as === "style" || as === "font");
|
|
40
|
+
}
|
|
41
|
+
return (tag === 'SCRIPT' ||
|
|
42
|
+
tag === 'NOSCRIPT' ||
|
|
43
|
+
tag === 'META' ||
|
|
44
|
+
tag === 'TITLE' ||
|
|
45
|
+
tag === 'BASE');
|
|
46
|
+
}
|
|
47
|
+
function isRootNode(node) {
|
|
48
|
+
return isInstance(node, Document) || isInstance(node, ShadowRoot);
|
|
49
|
+
}
|
|
50
|
+
function isObservable(node) {
|
|
51
|
+
if (isRootNode(node)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return !isIgnored(node);
|
|
55
|
+
}
|
|
56
|
+
export default class Observer {
|
|
57
|
+
constructor(app, options, context = window) {
|
|
58
|
+
this.app = app;
|
|
59
|
+
this.context = context;
|
|
60
|
+
this.commited = [];
|
|
61
|
+
this.recents = [];
|
|
62
|
+
this.myNodes = [];
|
|
63
|
+
this.indexes = [];
|
|
64
|
+
this.attributesList = [];
|
|
65
|
+
this.textSet = new Set();
|
|
66
|
+
this.textMasked = new Set();
|
|
67
|
+
this.options = Object.assign({
|
|
68
|
+
obscureTextEmails: true,
|
|
69
|
+
obscureTextNumbers: false,
|
|
70
|
+
}, options);
|
|
71
|
+
this.inUpperContext = context.parent === context;
|
|
72
|
+
this.observer = new MutationObserver(this.app.safe((mutations) => {
|
|
73
|
+
for (const mutation of mutations) {
|
|
74
|
+
const target = mutation.target;
|
|
75
|
+
const type = mutation.type;
|
|
76
|
+
// TODO TODO TODO: move to iframe_observer/remove??? (check if )
|
|
77
|
+
// Special case
|
|
78
|
+
// 'childList' on Document might happen in case of iframe.
|
|
79
|
+
// TODO: generalize as much as possible
|
|
80
|
+
// if (isInstance(target, Document) // Also ShadowRoot can be here
|
|
81
|
+
// && type === 'childList'
|
|
82
|
+
// //&& new Array(mutation.addedNodes).some(node => isInstance(node, HTMLHtmlElement))
|
|
83
|
+
// ) {
|
|
84
|
+
// const parentFrame = target.defaultView?.frameElement
|
|
85
|
+
// if (!parentFrame) { continue }
|
|
86
|
+
// this.bindTree(target.documentElement)
|
|
87
|
+
// const frameID = this.app.nodes.getID(parentFrame)
|
|
88
|
+
// const docID = this.app.nodes.getID(target.documentElement)
|
|
89
|
+
// if (frameID === undefined || docID === undefined) { continue }
|
|
90
|
+
// this.app.send(CreateIFrameDocument(frameID, docID));
|
|
91
|
+
// continue;
|
|
92
|
+
// }
|
|
93
|
+
if (!isObservable(target) || !context.document.contains(target)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (type === 'childList') {
|
|
97
|
+
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
|
98
|
+
this.bindTree(mutation.removedNodes[i]);
|
|
99
|
+
}
|
|
100
|
+
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
101
|
+
this.bindTree(mutation.addedNodes[i]);
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const id = this.app.nodes.getID(target);
|
|
106
|
+
if (id === undefined) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (id >= this.recents.length) { // TODO: something more convinient
|
|
110
|
+
this.recents[id] = undefined;
|
|
111
|
+
}
|
|
112
|
+
if (type === 'attributes') {
|
|
113
|
+
const name = mutation.attributeName;
|
|
114
|
+
if (name === null) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
let attr = this.attributesList[id];
|
|
118
|
+
if (attr === undefined) {
|
|
119
|
+
this.attributesList[id] = attr = new Set();
|
|
120
|
+
}
|
|
121
|
+
attr.add(name);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (type === 'characterData') {
|
|
125
|
+
this.textSet.add(id);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.commitNodes();
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
clear() {
|
|
133
|
+
this.commited.length = 0;
|
|
134
|
+
this.recents.length = 0;
|
|
135
|
+
this.indexes.length = 1;
|
|
136
|
+
this.attributesList.length = 0;
|
|
137
|
+
this.textSet.clear();
|
|
138
|
+
//this.textMasked.clear();
|
|
139
|
+
}
|
|
140
|
+
sendNodeAttribute(id, node, name, value) {
|
|
141
|
+
if (isSVGElement(node)) {
|
|
142
|
+
if (name.substr(0, 6) === 'xlink:') {
|
|
143
|
+
name = name.substr(6);
|
|
144
|
+
}
|
|
145
|
+
if (value === null) {
|
|
146
|
+
this.app.send(new RemoveNodeAttribute(id, name));
|
|
147
|
+
}
|
|
148
|
+
else if (name === 'href') {
|
|
149
|
+
if (value.length > 1e5) {
|
|
150
|
+
value = '';
|
|
151
|
+
}
|
|
152
|
+
this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
this.app.send(new SetNodeAttribute(id, name, value));
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (name === 'src' ||
|
|
160
|
+
name === 'srcset' ||
|
|
161
|
+
name === 'integrity' ||
|
|
162
|
+
name === 'crossorigin' ||
|
|
163
|
+
name === 'autocomplete' ||
|
|
164
|
+
name.substr(0, 2) === 'on') {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (name === 'value' &&
|
|
168
|
+
isInstance(node, HTMLInputElement) &&
|
|
169
|
+
node.type !== 'button' &&
|
|
170
|
+
node.type !== 'reset' &&
|
|
171
|
+
node.type !== 'submit') {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (value === null) {
|
|
175
|
+
this.app.send(new RemoveNodeAttribute(id, name));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (name === 'style' || name === 'href' && isInstance(node, HTMLLinkElement)) {
|
|
179
|
+
this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (name === 'href' || value.length > 1e5) {
|
|
183
|
+
value = '';
|
|
184
|
+
}
|
|
185
|
+
this.app.send(new SetNodeAttribute(id, name, value));
|
|
186
|
+
}
|
|
187
|
+
/* TODO: abstract sanitation */
|
|
188
|
+
getInnerTextSecure(el) {
|
|
189
|
+
const id = this.app.nodes.getID(el);
|
|
190
|
+
if (!id) {
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
return this.checkObscure(id, el.innerText);
|
|
194
|
+
}
|
|
195
|
+
checkObscure(id, data) {
|
|
196
|
+
if (this.textMasked.has(id)) {
|
|
197
|
+
return data.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█');
|
|
198
|
+
}
|
|
199
|
+
if (this.options.obscureTextNumbers) {
|
|
200
|
+
data = data.replace(/\d/g, '0');
|
|
201
|
+
}
|
|
202
|
+
if (this.options.obscureTextEmails) {
|
|
203
|
+
data = data.replace(/([^\s]+)@([^\s]+)\.([^\s]+)/g, (...f) => stars(f[1]) + '@' + stars(f[2]) + '.' + stars(f[3]));
|
|
204
|
+
}
|
|
205
|
+
return data;
|
|
206
|
+
}
|
|
207
|
+
sendNodeData(id, parentElement, data) {
|
|
208
|
+
if (isInstance(parentElement, HTMLStyleElement) || isInstance(parentElement, SVGStyleElement)) {
|
|
209
|
+
this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref()));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
data = this.checkObscure(id, data);
|
|
213
|
+
this.app.send(new SetNodeData(id, data));
|
|
214
|
+
}
|
|
215
|
+
/* end TODO: abstract sanitation */
|
|
216
|
+
bindNode(node) {
|
|
217
|
+
const r = this.app.nodes.registerNode(node);
|
|
218
|
+
const id = r[0];
|
|
219
|
+
this.recents[id] = r[1] || this.recents[id] || false;
|
|
220
|
+
this.myNodes[id] = true;
|
|
221
|
+
}
|
|
222
|
+
bindTree(node) {
|
|
223
|
+
if (!isObservable(node)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.bindNode(node);
|
|
227
|
+
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
|
|
228
|
+
acceptNode: (node) => isIgnored(node) || this.app.nodes.getID(node) !== undefined
|
|
229
|
+
? NodeFilter.FILTER_REJECT
|
|
230
|
+
: NodeFilter.FILTER_ACCEPT,
|
|
231
|
+
},
|
|
232
|
+
// @ts-ignore
|
|
233
|
+
false);
|
|
234
|
+
while (walker.nextNode()) {
|
|
235
|
+
this.bindNode(walker.currentNode);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
unbindNode(node) {
|
|
239
|
+
const id = this.app.nodes.unregisterNode(node);
|
|
240
|
+
if (id !== undefined && this.recents[id] === false) {
|
|
241
|
+
this.app.send(new RemoveNode(id));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
_commitNode(id, node) {
|
|
245
|
+
if (isRootNode(node)) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
const parent = node.parentNode;
|
|
249
|
+
let parentID;
|
|
250
|
+
// Disable parent check for the upper context HTMLHtmlElement, because it is root there... (before)
|
|
251
|
+
// TODO: get rid of "special" cases (there is an issue with CreateDocument altered behaviour though)
|
|
252
|
+
// TODO: Clean the logic (though now it workd fine)
|
|
253
|
+
if (!isInstance(node, HTMLHtmlElement) || !this.inUpperContext) {
|
|
254
|
+
if (parent === null) {
|
|
255
|
+
this.unbindNode(node);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
parentID = this.app.nodes.getID(parent);
|
|
259
|
+
if (parentID === undefined) {
|
|
260
|
+
this.unbindNode(node);
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
if (!this.commitNode(parentID)) {
|
|
264
|
+
this.unbindNode(node);
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
if (this.textMasked.has(parentID) ||
|
|
268
|
+
(isInstance(node, Element) && hasOpenreplayAttribute(node, 'masked'))) {
|
|
269
|
+
this.textMasked.add(id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
let sibling = node.previousSibling;
|
|
273
|
+
while (sibling !== null) {
|
|
274
|
+
const siblingID = this.app.nodes.getID(sibling);
|
|
275
|
+
if (siblingID !== undefined) {
|
|
276
|
+
this.commitNode(siblingID);
|
|
277
|
+
this.indexes[id] = this.indexes[siblingID] + 1;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
sibling = sibling.previousSibling;
|
|
281
|
+
}
|
|
282
|
+
if (sibling === null) {
|
|
283
|
+
this.indexes[id] = 0; //
|
|
284
|
+
}
|
|
285
|
+
const isNew = this.recents[id];
|
|
286
|
+
const index = this.indexes[id];
|
|
287
|
+
if (index === undefined) {
|
|
288
|
+
throw 'commitNode: missing node index';
|
|
289
|
+
}
|
|
290
|
+
if (isNew === true) {
|
|
291
|
+
if (isInstance(node, Element)) {
|
|
292
|
+
if (parentID !== undefined) {
|
|
293
|
+
this.app.send(new CreateElementNode(id, parentID, index, node.tagName, isSVGElement(node)));
|
|
294
|
+
}
|
|
295
|
+
for (let i = 0; i < node.attributes.length; i++) {
|
|
296
|
+
const attr = node.attributes[i];
|
|
297
|
+
this.sendNodeAttribute(id, node, attr.nodeName, attr.value);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else if (isInstance(node, Text)) {
|
|
301
|
+
// for text node id != 0, hence parentID !== undefined and parent is Element
|
|
302
|
+
this.app.send(new CreateTextNode(id, parentID, index));
|
|
303
|
+
this.sendNodeData(id, parent, node.data);
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
if (isNew === false && parentID !== undefined) {
|
|
308
|
+
this.app.send(new MoveNode(id, parentID, index));
|
|
309
|
+
}
|
|
310
|
+
const attr = this.attributesList[id];
|
|
311
|
+
if (attr !== undefined) {
|
|
312
|
+
if (!isInstance(node, Element)) {
|
|
313
|
+
throw 'commitNode: node is not an element';
|
|
314
|
+
}
|
|
315
|
+
for (const name of attr) {
|
|
316
|
+
this.sendNodeAttribute(id, node, name, node.getAttribute(name));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (this.textSet.has(id)) {
|
|
320
|
+
if (!isInstance(node, Text)) {
|
|
321
|
+
throw 'commitNode: node is not a text';
|
|
322
|
+
}
|
|
323
|
+
// for text node id != 0, hence parent is Element
|
|
324
|
+
this.sendNodeData(id, parent, node.data);
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
commitNode(id) {
|
|
329
|
+
const node = this.app.nodes.getNode(id);
|
|
330
|
+
if (node === undefined) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
const cmt = this.commited[id];
|
|
334
|
+
if (cmt !== undefined) {
|
|
335
|
+
return cmt;
|
|
336
|
+
}
|
|
337
|
+
return (this.commited[id] = this._commitNode(id, node));
|
|
338
|
+
}
|
|
339
|
+
commitNodes() {
|
|
340
|
+
let node;
|
|
341
|
+
for (let id = 0; id < this.recents.length; id++) {
|
|
342
|
+
// TODO: make things/logic nice here.
|
|
343
|
+
// commit required in any case if recents[id] true or false (in case of unbinding).
|
|
344
|
+
// ???!?!?R@TW:$HKJ$WLKn
|
|
345
|
+
if (!this.myNodes[id]) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
this.commitNode(id);
|
|
349
|
+
if (this.recents[id] === true && (node = this.app.nodes.getNode(id))) {
|
|
350
|
+
this.app.nodes.callNodeCallbacks(node);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
this.clear();
|
|
354
|
+
}
|
|
355
|
+
// ISSSUE
|
|
356
|
+
observeRoot(node, beforeCommit, nodeToBind = node) {
|
|
357
|
+
this.observer.observe(node, {
|
|
358
|
+
childList: true,
|
|
359
|
+
attributes: true,
|
|
360
|
+
characterData: true,
|
|
361
|
+
subtree: true,
|
|
362
|
+
attributeOldValue: false,
|
|
363
|
+
characterDataOldValue: false,
|
|
364
|
+
});
|
|
365
|
+
this.bindTree(nodeToBind);
|
|
366
|
+
beforeCommit(this.app.nodes.getID(node));
|
|
367
|
+
this.commitNodes();
|
|
368
|
+
}
|
|
369
|
+
disconnect() {
|
|
370
|
+
this.observer.disconnect();
|
|
371
|
+
this.clear();
|
|
372
|
+
// to sanitizer
|
|
373
|
+
this.textMasked.clear();
|
|
374
|
+
this.myNodes.length = 0;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -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 type { Options as BaseOptions } from "./observer.js";
|
|
3
|
+
import App from "../index.js";
|
|
4
|
+
export interface Options extends Partial<BaseOptions> {
|
|
5
|
+
captureIFrames: boolean;
|
|
6
|
+
}
|
|
7
|
+
export default class TopObserver extends Observer<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,79 @@
|
|
|
1
|
+
import Observer, { isInstance } from "./observer.js";
|
|
2
|
+
import IFrameObserver from "./iframe_observer.js";
|
|
3
|
+
import ShadowRootObserver from "./shadow_root_observer.js";
|
|
4
|
+
import { CreateDocument } from "../../messages/index.js";
|
|
5
|
+
const attachShadowNativeFn = Element.prototype.attachShadow;
|
|
6
|
+
export default class TopObserver extends Observer {
|
|
7
|
+
constructor(app, options) {
|
|
8
|
+
super(app, Object.assign({
|
|
9
|
+
captureIFrames: false
|
|
10
|
+
}, options));
|
|
11
|
+
this.iframeObservers = [];
|
|
12
|
+
this.shadowRootObservers = [];
|
|
13
|
+
// IFrames
|
|
14
|
+
this.app.nodes.attachNodeCallback(node => {
|
|
15
|
+
if (isInstance(node, HTMLIFrameElement) &&
|
|
16
|
+
(this.options.captureIFrames || node.getAttribute("data-openreplay-capture"))) {
|
|
17
|
+
this.handleIframe(node);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
// ShadowDOM
|
|
21
|
+
this.app.nodes.attachNodeCallback(node => {
|
|
22
|
+
if (isInstance(node, Element) && node.shadowRoot !== null) {
|
|
23
|
+
this.handleShadowRoot(node.shadowRoot);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
handleIframe(iframe) {
|
|
28
|
+
let context = null;
|
|
29
|
+
const handle = this.app.safe(() => {
|
|
30
|
+
const id = this.app.nodes.getID(iframe);
|
|
31
|
+
if (id === undefined) {
|
|
32
|
+
return;
|
|
33
|
+
} //log
|
|
34
|
+
if (iframe.contentWindow === context) {
|
|
35
|
+
return;
|
|
36
|
+
} //Does this happen frequently?
|
|
37
|
+
context = iframe.contentWindow;
|
|
38
|
+
if (!context) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const observer = new IFrameObserver(this.app, this.options, context);
|
|
42
|
+
this.iframeObservers.push(observer);
|
|
43
|
+
observer.observe(iframe);
|
|
44
|
+
});
|
|
45
|
+
this.app.attachEventListener(iframe, "load", handle);
|
|
46
|
+
handle();
|
|
47
|
+
}
|
|
48
|
+
handleShadowRoot(shRoot) {
|
|
49
|
+
const observer = new ShadowRootObserver(this.app, this.options, this.context);
|
|
50
|
+
this.shadowRootObservers.push(observer);
|
|
51
|
+
observer.observe(shRoot.host);
|
|
52
|
+
}
|
|
53
|
+
observe() {
|
|
54
|
+
// Protection from several subsequent calls?
|
|
55
|
+
const observer = this;
|
|
56
|
+
Element.prototype.attachShadow = function () {
|
|
57
|
+
const shadow = attachShadowNativeFn.apply(this, arguments);
|
|
58
|
+
observer.handleShadowRoot(shadow);
|
|
59
|
+
return shadow;
|
|
60
|
+
};
|
|
61
|
+
// Can observe documentElement (<html>) here, because it is not supposed to be changing.
|
|
62
|
+
// However, it is possible in some exotic cases and may cause an ignorance of the newly created <html>
|
|
63
|
+
// In this case context.document have to be observed, but this will cause
|
|
64
|
+
// the change in the re-player behaviour caused by CreateDocument message:
|
|
65
|
+
// the 0-node ("fRoot") will become #document rather than documentElement as it is now.
|
|
66
|
+
// Alternatively - observe(#document) then bindNode(documentElement)
|
|
67
|
+
this.observeRoot(this.context.document, () => {
|
|
68
|
+
this.app.send(new CreateDocument());
|
|
69
|
+
}, this.context.document.documentElement);
|
|
70
|
+
}
|
|
71
|
+
disconnect() {
|
|
72
|
+
Element.prototype.attachShadow = attachShadowNativeFn;
|
|
73
|
+
this.iframeObservers.forEach(o => o.disconnect());
|
|
74
|
+
this.iframeObservers = [];
|
|
75
|
+
this.shadowRootObservers.forEach(o => o.disconnect());
|
|
76
|
+
this.shadowRootObservers = [];
|
|
77
|
+
super.disconnect();
|
|
78
|
+
}
|
|
79
|
+
}
|
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.15',
|
|
115
115
|
projectKey: options.projectKey,
|
|
116
116
|
doNotTrack,
|
|
117
117
|
// TODO: add precise reason (an exact API missing)
|