@openreplay/tracker 3.4.13 → 3.4.14
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 +3 -3
- package/cjs/app/observer/iframe_observer.d.ts +4 -0
- package/cjs/app/observer/iframe_observer.js +22 -0
- package/cjs/app/{observer.d.ts → observer/observer.d.ts} +17 -14
- package/cjs/app/{observer.js → observer/observer.js} +128 -142
- 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 +86 -0
- package/cjs/index.js +1 -1
- package/lib/app/index.d.ts +2 -2
- package/lib/app/index.js +2 -2
- package/lib/app/observer/iframe_observer.d.ts +4 -0
- package/lib/app/observer/iframe_observer.js +19 -0
- package/lib/app/{observer.d.ts → observer/observer.d.ts} +17 -14
- package/lib/app/{observer.js → observer/observer.js} +126 -142
- 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 +83 -0
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/cjs/index.js
CHANGED
|
@@ -115,7 +115,7 @@ class API {
|
|
|
115
115
|
// no-cors issue only with text/plain or not-set Content-Type
|
|
116
116
|
// req.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
|
117
117
|
req.send(JSON.stringify({
|
|
118
|
-
trackerVersion: '3.4.
|
|
118
|
+
trackerVersion: '3.4.14',
|
|
119
119
|
projectKey: options.projectKey,
|
|
120
120
|
doNotTrack,
|
|
121
121
|
// TODO: add precise reason (an exact API missing)
|
package/lib/app/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import Message from "../messages/message.js";
|
|
2
2
|
import Nodes from "./nodes.js";
|
|
3
|
-
import Observer from "./observer.js";
|
|
3
|
+
import Observer from "./observer/top_observer.js";
|
|
4
4
|
import Ticker from "./ticker.js";
|
|
5
|
-
import type { Options as ObserverOptions } from "./observer.js";
|
|
5
|
+
import type { Options as ObserverOptions } from "./observer/top_observer.js";
|
|
6
6
|
import type { Options as WebworkerOptions } from "../messages/webworker.js";
|
|
7
7
|
export interface OnStartInfo {
|
|
8
8
|
sessionID: string;
|
package/lib/app/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { timestamp, log, warn } from "../utils.js";
|
|
2
2
|
import { Timestamp } from "../messages/index.js";
|
|
3
3
|
import Nodes from "./nodes.js";
|
|
4
|
-
import Observer from "./observer.js";
|
|
4
|
+
import Observer from "./observer/top_observer.js";
|
|
5
5
|
import Ticker from "./ticker.js";
|
|
6
6
|
import { deviceMemory, jsHeapSizeLimit } from "../modules/performance.js";
|
|
7
7
|
// TODO: use backendHost only
|
|
@@ -14,7 +14,7 @@ export default class App {
|
|
|
14
14
|
this.commitCallbacks = [];
|
|
15
15
|
this._sessionID = null;
|
|
16
16
|
this.isActive = false;
|
|
17
|
-
this.version = '3.4.
|
|
17
|
+
this.version = '3.4.14';
|
|
18
18
|
this.projectKey = projectKey;
|
|
19
19
|
this.options = Object.assign({
|
|
20
20
|
revID: '',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Observer from "./observer.js";
|
|
2
|
+
import { CreateIFrameDocument } from "../../messages/index.js";
|
|
3
|
+
export default class IFrameObserver extends Observer {
|
|
4
|
+
observe(iframe) {
|
|
5
|
+
const doc = iframe.contentDocument;
|
|
6
|
+
const hostID = this.app.nodes.getID(iframe);
|
|
7
|
+
if (!doc || hostID === undefined) {
|
|
8
|
+
return;
|
|
9
|
+
} //log TODO common app.logger
|
|
10
|
+
// Have to observe document, because the inner <html> might be changed
|
|
11
|
+
this.observeRoot(doc, (docID) => {
|
|
12
|
+
if (docID === undefined) {
|
|
13
|
+
console.log("OpenReplay: Iframe document not bound");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
this.app.send(CreateIFrameDocument(hostID, docID));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import App from "
|
|
2
|
-
interface Window extends
|
|
1
|
+
import App from "../index.js";
|
|
2
|
+
export interface Window extends globalThis.Window {
|
|
3
3
|
HTMLInputElement: typeof HTMLInputElement;
|
|
4
4
|
HTMLLinkElement: typeof HTMLLinkElement;
|
|
5
5
|
HTMLStyleElement: typeof HTMLStyleElement;
|
|
@@ -7,27 +7,33 @@ interface Window extends WindowProxy {
|
|
|
7
7
|
HTMLIFrameElement: typeof HTMLIFrameElement;
|
|
8
8
|
Text: typeof Text;
|
|
9
9
|
Element: typeof Element;
|
|
10
|
+
ShadowRoot: typeof ShadowRoot;
|
|
10
11
|
}
|
|
12
|
+
declare type WindowConstructor = Document | Element | Text | ShadowRoot | HTMLInputElement | HTMLLinkElement | HTMLStyleElement | HTMLIFrameElement;
|
|
13
|
+
declare type Constructor<T> = {
|
|
14
|
+
new (...args: any[]): T;
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function isInstance<T extends WindowConstructor>(node: Node, constr: Constructor<T>): node is T;
|
|
11
18
|
export interface Options {
|
|
12
19
|
obscureTextEmails: boolean;
|
|
13
20
|
obscureTextNumbers: boolean;
|
|
14
|
-
captureIFrames: boolean;
|
|
15
21
|
}
|
|
16
|
-
export default class Observer {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
private readonly context;
|
|
22
|
+
export default abstract class Observer<AdditionalOptions = {}> {
|
|
23
|
+
protected readonly app: App;
|
|
24
|
+
protected readonly context: Window;
|
|
20
25
|
private readonly observer;
|
|
21
26
|
private readonly commited;
|
|
22
27
|
private readonly recents;
|
|
28
|
+
private readonly myNodes;
|
|
23
29
|
private readonly indexes;
|
|
24
30
|
private readonly attributesList;
|
|
25
31
|
private readonly textSet;
|
|
26
32
|
private readonly textMasked;
|
|
27
|
-
|
|
33
|
+
protected readonly options: Options & AdditionalOptions;
|
|
34
|
+
private readonly inUpperContext;
|
|
35
|
+
constructor(app: App, options: Partial<Options> & AdditionalOptions, context?: Window);
|
|
28
36
|
private clear;
|
|
29
|
-
private isInstance;
|
|
30
|
-
private isIgnored;
|
|
31
37
|
private sendNodeAttribute;
|
|
32
38
|
getInnerTextSecure(el: HTMLElement): string;
|
|
33
39
|
private checkObscure;
|
|
@@ -38,10 +44,7 @@ export default class Observer {
|
|
|
38
44
|
private _commitNode;
|
|
39
45
|
private commitNode;
|
|
40
46
|
private commitNodes;
|
|
41
|
-
|
|
42
|
-
private handleIframe;
|
|
43
|
-
private observeIframe;
|
|
44
|
-
observe(): void;
|
|
47
|
+
protected observeRoot(node: Node, beforeCommit: (id?: number) => unknown, nodeToBind?: Node): void;
|
|
45
48
|
disconnect(): void;
|
|
46
49
|
}
|
|
47
50
|
export {};
|
|
@@ -1,40 +1,96 @@
|
|
|
1
|
-
import { stars, hasOpenreplayAttribute } from "
|
|
2
|
-
import {
|
|
1
|
+
import { stars, hasOpenreplayAttribute } from "../../utils.js";
|
|
2
|
+
import { RemoveNodeAttribute, SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, SetNodeData, CreateTextNode, CreateElementNode, MoveNode, RemoveNode, } from "../../messages/index.js";
|
|
3
3
|
function isSVGElement(node) {
|
|
4
4
|
return node.namespaceURI === 'http://www.w3.org/2000/svg';
|
|
5
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
|
+
}
|
|
6
56
|
export default class Observer {
|
|
7
57
|
constructor(app, options, context = window) {
|
|
8
58
|
this.app = app;
|
|
9
|
-
this.options = options;
|
|
10
59
|
this.context = context;
|
|
11
|
-
this.
|
|
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;
|
|
12
72
|
this.observer = new MutationObserver(this.app.safe((mutations) => {
|
|
13
|
-
var _a;
|
|
14
73
|
for (const mutation of mutations) {
|
|
15
74
|
const target = mutation.target;
|
|
16
75
|
const type = mutation.type;
|
|
76
|
+
// TODO TODO TODO: move to iframe_observer/remove??? (check if )
|
|
17
77
|
// Special case
|
|
18
|
-
//
|
|
78
|
+
// 'childList' on Document might happen in case of iframe.
|
|
19
79
|
// TODO: generalize as much as possible
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
//&& new Array(mutation.addedNodes).some(node =>
|
|
23
|
-
) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.app.send(CreateIFrameDocument(frameID, docID));
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
if (this.isIgnored(target) || !context.document.contains(target)) {
|
|
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)) {
|
|
38
94
|
continue;
|
|
39
95
|
}
|
|
40
96
|
if (type === 'childList') {
|
|
@@ -50,7 +106,7 @@ export default class Observer {
|
|
|
50
106
|
if (id === undefined) {
|
|
51
107
|
continue;
|
|
52
108
|
}
|
|
53
|
-
if (id >= this.recents.length) {
|
|
109
|
+
if (id >= this.recents.length) { // TODO: something more convinient
|
|
54
110
|
this.recents[id] = undefined;
|
|
55
111
|
}
|
|
56
112
|
if (type === 'attributes') {
|
|
@@ -72,12 +128,6 @@ export default class Observer {
|
|
|
72
128
|
}
|
|
73
129
|
this.commitNodes();
|
|
74
130
|
}));
|
|
75
|
-
this.commited = [];
|
|
76
|
-
this.recents = [];
|
|
77
|
-
this.indexes = [0];
|
|
78
|
-
this.attributesList = [];
|
|
79
|
-
this.textSet = new Set();
|
|
80
|
-
this.textMasked = new Set();
|
|
81
131
|
}
|
|
82
132
|
clear() {
|
|
83
133
|
this.commited.length = 0;
|
|
@@ -85,40 +135,7 @@ export default class Observer {
|
|
|
85
135
|
this.indexes.length = 1;
|
|
86
136
|
this.attributesList.length = 0;
|
|
87
137
|
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');
|
|
138
|
+
//this.textMasked.clear();
|
|
122
139
|
}
|
|
123
140
|
sendNodeAttribute(id, node, name, value) {
|
|
124
141
|
if (isSVGElement(node)) {
|
|
@@ -148,7 +165,7 @@ export default class Observer {
|
|
|
148
165
|
return;
|
|
149
166
|
}
|
|
150
167
|
if (name === 'value' &&
|
|
151
|
-
|
|
168
|
+
isInstance(node, HTMLInputElement) &&
|
|
152
169
|
node.type !== 'button' &&
|
|
153
170
|
node.type !== 'reset' &&
|
|
154
171
|
node.type !== 'submit') {
|
|
@@ -158,7 +175,7 @@ export default class Observer {
|
|
|
158
175
|
this.app.send(new RemoveNodeAttribute(id, name));
|
|
159
176
|
return;
|
|
160
177
|
}
|
|
161
|
-
if (name === 'style' || name === 'href' &&
|
|
178
|
+
if (name === 'style' || name === 'href' && isInstance(node, HTMLLinkElement)) {
|
|
162
179
|
this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
|
|
163
180
|
return;
|
|
164
181
|
}
|
|
@@ -188,7 +205,7 @@ export default class Observer {
|
|
|
188
205
|
return data;
|
|
189
206
|
}
|
|
190
207
|
sendNodeData(id, parentElement, data) {
|
|
191
|
-
if (
|
|
208
|
+
if (isInstance(parentElement, HTMLStyleElement) || isInstance(parentElement, SVGStyleElement)) {
|
|
192
209
|
this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref()));
|
|
193
210
|
return;
|
|
194
211
|
}
|
|
@@ -200,14 +217,15 @@ export default class Observer {
|
|
|
200
217
|
const r = this.app.nodes.registerNode(node);
|
|
201
218
|
const id = r[0];
|
|
202
219
|
this.recents[id] = r[1] || this.recents[id] || false;
|
|
220
|
+
this.myNodes[id] = true;
|
|
203
221
|
}
|
|
204
222
|
bindTree(node) {
|
|
205
|
-
if (
|
|
223
|
+
if (!isObservable(node)) {
|
|
206
224
|
return;
|
|
207
225
|
}
|
|
208
226
|
this.bindNode(node);
|
|
209
227
|
const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
|
|
210
|
-
acceptNode: (node) =>
|
|
228
|
+
acceptNode: (node) => isIgnored(node) || this.app.nodes.getID(node) !== undefined
|
|
211
229
|
? NodeFilter.FILTER_REJECT
|
|
212
230
|
: NodeFilter.FILTER_ACCEPT,
|
|
213
231
|
},
|
|
@@ -224,12 +242,15 @@ export default class Observer {
|
|
|
224
242
|
}
|
|
225
243
|
}
|
|
226
244
|
_commitNode(id, node) {
|
|
245
|
+
if (isRootNode(node)) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
227
248
|
const parent = node.parentNode;
|
|
228
249
|
let parentID;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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) {
|
|
233
254
|
if (parent === null) {
|
|
234
255
|
this.unbindNode(node);
|
|
235
256
|
return false;
|
|
@@ -244,22 +265,22 @@ export default class Observer {
|
|
|
244
265
|
return false;
|
|
245
266
|
}
|
|
246
267
|
if (this.textMasked.has(parentID) ||
|
|
247
|
-
(
|
|
268
|
+
(isInstance(node, Element) && hasOpenreplayAttribute(node, 'masked'))) {
|
|
248
269
|
this.textMasked.add(id);
|
|
249
270
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
sibling = sibling.previousSibling;
|
|
259
|
-
}
|
|
260
|
-
if (sibling === null) {
|
|
261
|
-
this.indexes[id] = 0;
|
|
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;
|
|
262
279
|
}
|
|
280
|
+
sibling = sibling.previousSibling;
|
|
281
|
+
}
|
|
282
|
+
if (sibling === null) {
|
|
283
|
+
this.indexes[id] = 0; //
|
|
263
284
|
}
|
|
264
285
|
const isNew = this.recents[id];
|
|
265
286
|
const index = this.indexes[id];
|
|
@@ -267,7 +288,7 @@ export default class Observer {
|
|
|
267
288
|
throw 'commitNode: missing node index';
|
|
268
289
|
}
|
|
269
290
|
if (isNew === true) {
|
|
270
|
-
if (
|
|
291
|
+
if (isInstance(node, Element)) {
|
|
271
292
|
if (parentID !== undefined) {
|
|
272
293
|
this.app.send(new CreateElementNode(id, parentID, index, node.tagName, isSVGElement(node)));
|
|
273
294
|
}
|
|
@@ -275,12 +296,8 @@ export default class Observer {
|
|
|
275
296
|
const attr = node.attributes[i];
|
|
276
297
|
this.sendNodeAttribute(id, node, attr.nodeName, attr.value);
|
|
277
298
|
}
|
|
278
|
-
if (this.isInstance(node, HTMLIFrameElement) &&
|
|
279
|
-
(this.options.captureIFrames || node.getAttribute("data-openreplay-capture"))) {
|
|
280
|
-
this.handleIframe(node);
|
|
281
|
-
}
|
|
282
299
|
}
|
|
283
|
-
else if (
|
|
300
|
+
else if (isInstance(node, Text)) {
|
|
284
301
|
// for text node id != 0, hence parentID !== undefined and parent is Element
|
|
285
302
|
this.app.send(new CreateTextNode(id, parentID, index));
|
|
286
303
|
this.sendNodeData(id, parent, node.data);
|
|
@@ -292,7 +309,7 @@ export default class Observer {
|
|
|
292
309
|
}
|
|
293
310
|
const attr = this.attributesList[id];
|
|
294
311
|
if (attr !== undefined) {
|
|
295
|
-
if (!
|
|
312
|
+
if (!isInstance(node, Element)) {
|
|
296
313
|
throw 'commitNode: node is not an element';
|
|
297
314
|
}
|
|
298
315
|
for (const name of attr) {
|
|
@@ -300,7 +317,7 @@ export default class Observer {
|
|
|
300
317
|
}
|
|
301
318
|
}
|
|
302
319
|
if (this.textSet.has(id)) {
|
|
303
|
-
if (!
|
|
320
|
+
if (!isInstance(node, Text)) {
|
|
304
321
|
throw 'commitNode: node is not a text';
|
|
305
322
|
}
|
|
306
323
|
// for text node id != 0, hence parent is Element
|
|
@@ -322,6 +339,12 @@ export default class Observer {
|
|
|
322
339
|
commitNodes() {
|
|
323
340
|
let node;
|
|
324
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
|
+
}
|
|
325
348
|
this.commitNode(id);
|
|
326
349
|
if (this.recents[id] === true && (node = this.app.nodes.getNode(id))) {
|
|
327
350
|
this.app.nodes.callNodeCallbacks(node);
|
|
@@ -329,31 +352,9 @@ export default class Observer {
|
|
|
329
352
|
}
|
|
330
353
|
this.clear();
|
|
331
354
|
}
|
|
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, {
|
|
355
|
+
// ISSSUE
|
|
356
|
+
observeRoot(node, beforeCommit, nodeToBind = node) {
|
|
357
|
+
this.observer.observe(node, {
|
|
357
358
|
childList: true,
|
|
358
359
|
attributes: true,
|
|
359
360
|
characterData: true,
|
|
@@ -361,32 +362,15 @@ export default class Observer {
|
|
|
361
362
|
attributeOldValue: false,
|
|
362
363
|
characterDataOldValue: false,
|
|
363
364
|
});
|
|
364
|
-
this.bindTree(
|
|
365
|
-
|
|
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, {
|
|
375
|
-
childList: true,
|
|
376
|
-
attributes: true,
|
|
377
|
-
characterData: true,
|
|
378
|
-
subtree: true,
|
|
379
|
-
attributeOldValue: false,
|
|
380
|
-
characterDataOldValue: false,
|
|
381
|
-
});
|
|
382
|
-
this.app.send(new CreateDocument());
|
|
383
|
-
this.bindTree(this.context.document.documentElement);
|
|
365
|
+
this.bindTree(nodeToBind);
|
|
366
|
+
beforeCommit(this.app.nodes.getID(node));
|
|
384
367
|
this.commitNodes();
|
|
385
368
|
}
|
|
386
369
|
disconnect() {
|
|
387
|
-
this.iframeObservers.forEach(o => o.disconnect());
|
|
388
|
-
this.iframeObservers = [];
|
|
389
370
|
this.observer.disconnect();
|
|
390
371
|
this.clear();
|
|
372
|
+
// to sanitizer
|
|
373
|
+
this.textMasked.clear();
|
|
374
|
+
this.myNodes.length = 0;
|
|
391
375
|
}
|
|
392
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,83 @@
|
|
|
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
|
+
// @ts-ignore
|
|
43
|
+
observer.commited = this.commited;
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
//observers.recents = this.recents
|
|
46
|
+
this.iframeObservers.push(observer);
|
|
47
|
+
observer.observe(iframe);
|
|
48
|
+
});
|
|
49
|
+
this.app.attachEventListener(iframe, "load", handle);
|
|
50
|
+
handle();
|
|
51
|
+
}
|
|
52
|
+
handleShadowRoot(shRoot) {
|
|
53
|
+
const observer = new ShadowRootObserver(this.app, this.options, this.context);
|
|
54
|
+
this.shadowRootObservers.push(observer);
|
|
55
|
+
observer.observe(shRoot.host);
|
|
56
|
+
}
|
|
57
|
+
observe() {
|
|
58
|
+
// Protection from several subsequent calls?
|
|
59
|
+
const observer = this;
|
|
60
|
+
Element.prototype.attachShadow = function () {
|
|
61
|
+
const shadow = attachShadowNativeFn.apply(this, arguments);
|
|
62
|
+
observer.handleShadowRoot(shadow);
|
|
63
|
+
return shadow;
|
|
64
|
+
};
|
|
65
|
+
// Can observe documentElement (<html>) here, because it is not supposed to be changing.
|
|
66
|
+
// 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:
|
|
69
|
+
// the 0-node ("fRoot") will become #document rather than documentElement as it is now.
|
|
70
|
+
// Alternatively - observe(#document) then bindNode(documentElement)
|
|
71
|
+
this.observeRoot(this.context.document, () => {
|
|
72
|
+
this.app.send(new CreateDocument());
|
|
73
|
+
}, this.context.document.documentElement);
|
|
74
|
+
}
|
|
75
|
+
disconnect() {
|
|
76
|
+
Element.prototype.attachShadow = attachShadowNativeFn;
|
|
77
|
+
this.iframeObservers.forEach(o => o.disconnect());
|
|
78
|
+
this.iframeObservers = [];
|
|
79
|
+
this.shadowRootObservers.forEach(o => o.disconnect());
|
|
80
|
+
this.shadowRootObservers = [];
|
|
81
|
+
super.disconnect();
|
|
82
|
+
}
|
|
83
|
+
}
|
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.14',
|
|
115
115
|
projectKey: options.projectKey,
|
|
116
116
|
doNotTrack,
|
|
117
117
|
// TODO: add precise reason (an exact API missing)
|