@openreplay/tracker 5.0.2-beta → 5.0.2

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.
Files changed (135) hide show
  1. package/cjs/app/guards.d.ts +21 -0
  2. package/cjs/app/guards.js +37 -0
  3. package/cjs/app/index.d.ts +118 -0
  4. package/cjs/app/index.js +438 -0
  5. package/cjs/app/logger.d.ts +26 -0
  6. package/cjs/app/logger.js +45 -0
  7. package/cjs/app/messages.gen.d.ts +63 -0
  8. package/cjs/app/messages.gen.js +551 -0
  9. package/cjs/app/nodes.d.ts +18 -0
  10. package/cjs/app/nodes.js +82 -0
  11. package/cjs/app/observer/iframe_observer.d.ts +4 -0
  12. package/cjs/app/observer/iframe_observer.js +23 -0
  13. package/cjs/app/observer/iframe_offsets.d.ts +8 -0
  14. package/cjs/app/observer/iframe_offsets.js +59 -0
  15. package/cjs/app/observer/observer.d.ts +23 -0
  16. package/cjs/app/observer/observer.js +340 -0
  17. package/cjs/app/observer/shadow_root_observer.d.ts +4 -0
  18. package/cjs/app/observer/shadow_root_observer.js +21 -0
  19. package/cjs/app/observer/top_observer.d.ts +24 -0
  20. package/cjs/app/observer/top_observer.js +113 -0
  21. package/cjs/app/sanitizer.d.ts +24 -0
  22. package/cjs/app/sanitizer.js +76 -0
  23. package/cjs/app/session.d.ts +38 -0
  24. package/cjs/app/session.js +114 -0
  25. package/cjs/app/ticker.d.ts +12 -0
  26. package/cjs/app/ticker.js +42 -0
  27. package/cjs/common/interaction.d.ts +24 -0
  28. package/cjs/common/interaction.js +2 -0
  29. package/cjs/common/messages.gen.d.ts +427 -0
  30. package/cjs/common/messages.gen.js +4 -0
  31. package/cjs/index.d.ts +47 -0
  32. package/cjs/index.js +254 -0
  33. package/cjs/modules/connection.d.ts +2 -0
  34. package/cjs/modules/connection.js +15 -0
  35. package/cjs/modules/console.d.ts +6 -0
  36. package/cjs/modules/console.js +119 -0
  37. package/cjs/modules/constructedStyleSheets.d.ts +4 -0
  38. package/cjs/modules/constructedStyleSheets.js +131 -0
  39. package/cjs/modules/cssrules.d.ts +2 -0
  40. package/cjs/modules/cssrules.js +99 -0
  41. package/cjs/modules/exception.d.ts +16 -0
  42. package/cjs/modules/exception.js +77 -0
  43. package/cjs/modules/focus.d.ts +2 -0
  44. package/cjs/modules/focus.js +45 -0
  45. package/cjs/modules/fonts.d.ts +2 -0
  46. package/cjs/modules/fonts.js +57 -0
  47. package/cjs/modules/img.d.ts +2 -0
  48. package/cjs/modules/img.js +110 -0
  49. package/cjs/modules/input.d.ts +16 -0
  50. package/cjs/modules/input.js +163 -0
  51. package/cjs/modules/mouse.d.ts +2 -0
  52. package/cjs/modules/mouse.js +148 -0
  53. package/cjs/modules/network.d.ts +28 -0
  54. package/cjs/modules/network.js +203 -0
  55. package/cjs/modules/performance.d.ts +7 -0
  56. package/cjs/modules/performance.js +53 -0
  57. package/cjs/modules/scroll.d.ts +2 -0
  58. package/cjs/modules/scroll.js +79 -0
  59. package/cjs/modules/timing.d.ts +7 -0
  60. package/cjs/modules/timing.js +160 -0
  61. package/cjs/modules/viewport.d.ts +2 -0
  62. package/cjs/modules/viewport.js +43 -0
  63. package/cjs/package.json +1 -0
  64. package/cjs/utils.d.ts +13 -0
  65. package/cjs/utils.js +71 -0
  66. package/cjs/vendors/finder/finder.d.ts +12 -0
  67. package/cjs/vendors/finder/finder.js +352 -0
  68. package/lib/app/guards.d.ts +21 -0
  69. package/lib/app/guards.js +26 -0
  70. package/lib/app/index.d.ts +118 -0
  71. package/lib/app/index.js +434 -0
  72. package/lib/app/logger.d.ts +26 -0
  73. package/lib/app/logger.js +41 -0
  74. package/lib/app/messages.gen.d.ts +63 -0
  75. package/lib/app/messages.gen.js +486 -0
  76. package/lib/app/nodes.d.ts +18 -0
  77. package/lib/app/nodes.js +79 -0
  78. package/lib/app/observer/iframe_observer.d.ts +4 -0
  79. package/lib/app/observer/iframe_observer.js +20 -0
  80. package/lib/app/observer/iframe_offsets.d.ts +8 -0
  81. package/lib/app/observer/iframe_offsets.js +56 -0
  82. package/lib/app/observer/observer.d.ts +23 -0
  83. package/lib/app/observer/observer.js +337 -0
  84. package/lib/app/observer/shadow_root_observer.d.ts +4 -0
  85. package/lib/app/observer/shadow_root_observer.js +18 -0
  86. package/lib/app/observer/top_observer.d.ts +24 -0
  87. package/lib/app/observer/top_observer.js +110 -0
  88. package/lib/app/sanitizer.d.ts +24 -0
  89. package/lib/app/sanitizer.js +72 -0
  90. package/lib/app/session.d.ts +38 -0
  91. package/lib/app/session.js +111 -0
  92. package/lib/app/ticker.d.ts +12 -0
  93. package/lib/app/ticker.js +39 -0
  94. package/lib/common/interaction.d.ts +24 -0
  95. package/lib/common/interaction.js +1 -0
  96. package/lib/common/messages.gen.d.ts +427 -0
  97. package/lib/common/messages.gen.js +3 -0
  98. package/lib/common/tsconfig.tsbuildinfo +1 -0
  99. package/lib/index.d.ts +47 -0
  100. package/lib/index.js +248 -0
  101. package/lib/modules/connection.d.ts +2 -0
  102. package/lib/modules/connection.js +12 -0
  103. package/lib/modules/console.d.ts +6 -0
  104. package/lib/modules/console.js +116 -0
  105. package/lib/modules/constructedStyleSheets.d.ts +4 -0
  106. package/lib/modules/constructedStyleSheets.js +126 -0
  107. package/lib/modules/cssrules.d.ts +2 -0
  108. package/lib/modules/cssrules.js +97 -0
  109. package/lib/modules/exception.d.ts +16 -0
  110. package/lib/modules/exception.js +71 -0
  111. package/lib/modules/focus.d.ts +2 -0
  112. package/lib/modules/focus.js +42 -0
  113. package/lib/modules/fonts.d.ts +2 -0
  114. package/lib/modules/fonts.js +54 -0
  115. package/lib/modules/img.d.ts +2 -0
  116. package/lib/modules/img.js +107 -0
  117. package/lib/modules/input.d.ts +16 -0
  118. package/lib/modules/input.js +158 -0
  119. package/lib/modules/mouse.d.ts +2 -0
  120. package/lib/modules/mouse.js +145 -0
  121. package/lib/modules/network.d.ts +28 -0
  122. package/lib/modules/network.js +200 -0
  123. package/lib/modules/performance.d.ts +7 -0
  124. package/lib/modules/performance.js +49 -0
  125. package/lib/modules/scroll.d.ts +2 -0
  126. package/lib/modules/scroll.js +76 -0
  127. package/lib/modules/timing.d.ts +7 -0
  128. package/lib/modules/timing.js +157 -0
  129. package/lib/modules/viewport.d.ts +2 -0
  130. package/lib/modules/viewport.js +40 -0
  131. package/lib/utils.d.ts +13 -0
  132. package/lib/utils.js +61 -0
  133. package/lib/vendors/finder/finder.d.ts +12 -0
  134. package/lib/vendors/finder/finder.js +348 -0
  135. package/package.json +1 -1
@@ -0,0 +1,337 @@
1
+ import { RemoveNodeAttribute, SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, SetNodeData, CreateTextNode, CreateElementNode, MoveNode, RemoveNode, } from '../messages.gen.js';
2
+ import { isRootNode, isTextNode, isElementNode, isSVGElement, hasTag, isCommentNode, } from '../guards.js';
3
+ function isIgnored(node) {
4
+ if (isCommentNode(node)) {
5
+ return true;
6
+ }
7
+ if (isTextNode(node)) {
8
+ return false;
9
+ }
10
+ if (!isElementNode(node)) {
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' || tag === 'NOSCRIPT' || tag === 'META' || tag === 'TITLE' || tag === 'BASE');
20
+ }
21
+ function isObservable(node) {
22
+ if (isRootNode(node)) {
23
+ return true;
24
+ }
25
+ return !isIgnored(node);
26
+ }
27
+ /*
28
+ TODO:
29
+ - fix unbinding logic + send all removals first (ensure sequence is correct)
30
+ - use document as a 0-node in the upper context (should be updated in player at first)
31
+ */
32
+ var RecentsType;
33
+ (function (RecentsType) {
34
+ RecentsType[RecentsType["New"] = 0] = "New";
35
+ RecentsType[RecentsType["Removed"] = 1] = "Removed";
36
+ RecentsType[RecentsType["Changed"] = 2] = "Changed";
37
+ })(RecentsType || (RecentsType = {}));
38
+ export default class Observer {
39
+ constructor(app, isTopContext = false) {
40
+ this.app = app;
41
+ this.isTopContext = isTopContext;
42
+ this.commited = [];
43
+ this.recents = new Map();
44
+ this.indexes = [];
45
+ this.attributesMap = new Map();
46
+ this.textSet = new Set();
47
+ this.observer = new MutationObserver(this.app.safe((mutations) => {
48
+ for (const mutation of mutations) {
49
+ // mutations order is sequential
50
+ const target = mutation.target;
51
+ const type = mutation.type;
52
+ if (!isObservable(target)) {
53
+ continue;
54
+ }
55
+ if (type === 'childList') {
56
+ for (let i = 0; i < mutation.removedNodes.length; i++) {
57
+ // Should be the same as bindTree(mutation.removedNodes[i]), but logic needs to be be untied
58
+ if (isObservable(mutation.removedNodes[i])) {
59
+ this.bindNode(mutation.removedNodes[i]);
60
+ }
61
+ }
62
+ for (let i = 0; i < mutation.addedNodes.length; i++) {
63
+ this.bindTree(mutation.addedNodes[i]);
64
+ }
65
+ continue;
66
+ }
67
+ const id = this.app.nodes.getID(target);
68
+ if (id === undefined) {
69
+ continue;
70
+ }
71
+ if (!this.recents.has(id)) {
72
+ this.recents.set(id, RecentsType.Changed); // TODO only when altered
73
+ }
74
+ if (type === 'attributes') {
75
+ const name = mutation.attributeName;
76
+ if (name === null) {
77
+ continue;
78
+ }
79
+ let attr = this.attributesMap.get(id);
80
+ if (attr === undefined) {
81
+ this.attributesMap.set(id, (attr = new Set()));
82
+ }
83
+ attr.add(name);
84
+ continue;
85
+ }
86
+ if (type === 'characterData') {
87
+ this.textSet.add(id);
88
+ continue;
89
+ }
90
+ }
91
+ this.commitNodes();
92
+ }));
93
+ }
94
+ clear() {
95
+ this.commited.length = 0;
96
+ this.recents.clear();
97
+ this.indexes.length = 1;
98
+ this.attributesMap.clear();
99
+ this.textSet.clear();
100
+ }
101
+ sendNodeAttribute(id, node, name, value) {
102
+ if (isSVGElement(node)) {
103
+ if (name.substr(0, 6) === 'xlink:') {
104
+ name = name.substr(6);
105
+ }
106
+ if (value === null) {
107
+ this.app.send(RemoveNodeAttribute(id, name));
108
+ }
109
+ else if (name === 'href') {
110
+ if (value.length > 1e5) {
111
+ value = '';
112
+ }
113
+ this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
114
+ }
115
+ else {
116
+ this.app.send(SetNodeAttribute(id, name, value));
117
+ }
118
+ return;
119
+ }
120
+ if (name === 'src' ||
121
+ name === 'srcset' ||
122
+ name === 'integrity' ||
123
+ name === 'crossorigin' ||
124
+ name === 'autocomplete' ||
125
+ name.substr(0, 2) === 'on') {
126
+ return;
127
+ }
128
+ if (name === 'value' &&
129
+ hasTag(node, 'input') &&
130
+ node.type !== 'button' &&
131
+ node.type !== 'reset' &&
132
+ node.type !== 'submit') {
133
+ return;
134
+ }
135
+ if (value === null) {
136
+ this.app.send(RemoveNodeAttribute(id, name));
137
+ return;
138
+ }
139
+ if (name === 'style' || (name === 'href' && hasTag(node, 'link'))) {
140
+ this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
141
+ return;
142
+ }
143
+ if (name === 'href' || value.length > 1e5) {
144
+ value = '';
145
+ }
146
+ this.app.send(SetNodeAttribute(id, name, value));
147
+ }
148
+ sendNodeData(id, parentElement, data) {
149
+ if (hasTag(parentElement, 'style')) {
150
+ this.app.send(SetCSSDataURLBased(id, data, this.app.getBaseHref()));
151
+ return;
152
+ }
153
+ data = this.app.sanitizer.sanitize(id, data);
154
+ this.app.send(SetNodeData(id, data));
155
+ }
156
+ bindNode(node) {
157
+ const [id, isNew] = this.app.nodes.registerNode(node);
158
+ if (isNew) {
159
+ this.recents.set(id, RecentsType.New);
160
+ }
161
+ else if (this.recents.get(id) !== RecentsType.New) {
162
+ this.recents.set(id, RecentsType.Removed);
163
+ }
164
+ }
165
+ bindTree(node) {
166
+ if (!isObservable(node)) {
167
+ return;
168
+ }
169
+ this.bindNode(node);
170
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
171
+ acceptNode: (node) => isIgnored(node) || this.app.nodes.getID(node) !== undefined
172
+ ? NodeFilter.FILTER_REJECT
173
+ : NodeFilter.FILTER_ACCEPT,
174
+ },
175
+ // @ts-ignore
176
+ false);
177
+ while (walker.nextNode()) {
178
+ this.bindNode(walker.currentNode);
179
+ }
180
+ }
181
+ unbindTree(node) {
182
+ const id = this.app.nodes.unregisterNode(node);
183
+ if (id !== undefined && this.recents.get(id) === RecentsType.Removed) {
184
+ // Sending RemoveNode only for parent to maintain
185
+ this.app.send(RemoveNode(id));
186
+ // Unregistering all the children in order to clear the memory
187
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
188
+ acceptNode: (node) => isIgnored(node) || this.app.nodes.getID(node) === undefined
189
+ ? NodeFilter.FILTER_REJECT
190
+ : NodeFilter.FILTER_ACCEPT,
191
+ },
192
+ // @ts-ignore
193
+ false);
194
+ while (walker.nextNode()) {
195
+ this.app.nodes.unregisterNode(walker.currentNode);
196
+ }
197
+ // MBTODO: count and send RemovedNodesCount (for the page crash detection in heuristics)
198
+ }
199
+ }
200
+ // A top-consumption function on the infinite lists test. (~1% of performance resources)
201
+ _commitNode(id, node) {
202
+ if (isRootNode(node)) {
203
+ return true;
204
+ }
205
+ const parent = node.parentNode;
206
+ let parentID;
207
+ // Disable parent check for the upper context HTMLHtmlElement, because it is root there... (before)
208
+ // TODO: get rid of "special" cases (there is an issue with CreateDocument altered behaviour though)
209
+ // TODO: Clean the logic (though now it workd fine)
210
+ if (!hasTag(node, 'html') || !this.isTopContext) {
211
+ if (parent === null) {
212
+ // Sometimes one observation contains attribute mutations for the removimg node, which gets ignored here.
213
+ // That shouldn't affect the visual rendering ( should it? maybe when transition applied? )
214
+ this.unbindTree(node);
215
+ return false;
216
+ }
217
+ parentID = this.app.nodes.getID(parent);
218
+ if (parentID === undefined) {
219
+ this.unbindTree(node);
220
+ return false;
221
+ }
222
+ if (!this.commitNode(parentID)) {
223
+ this.unbindTree(node);
224
+ return false;
225
+ }
226
+ this.app.sanitizer.handleNode(id, parentID, node);
227
+ if (this.app.sanitizer.isHidden(parentID)) {
228
+ return false;
229
+ }
230
+ }
231
+ // From here parentID === undefined if node is top context HTML node
232
+ let sibling = node.previousSibling;
233
+ while (sibling !== null) {
234
+ const siblingID = this.app.nodes.getID(sibling);
235
+ if (siblingID !== undefined) {
236
+ this.commitNode(siblingID);
237
+ this.indexes[id] = this.indexes[siblingID] + 1;
238
+ break;
239
+ }
240
+ sibling = sibling.previousSibling;
241
+ }
242
+ if (sibling === null) {
243
+ this.indexes[id] = 0;
244
+ }
245
+ const recentsType = this.recents.get(id);
246
+ const isNew = recentsType === RecentsType.New;
247
+ const index = this.indexes[id];
248
+ if (index === undefined) {
249
+ throw 'commitNode: missing node index';
250
+ }
251
+ if (isNew) {
252
+ if (isElementNode(node)) {
253
+ let el = node;
254
+ if (parentID !== undefined) {
255
+ if (this.app.sanitizer.isHidden(id)) {
256
+ const width = el.clientWidth;
257
+ const height = el.clientHeight;
258
+ el = node.cloneNode();
259
+ el.style.width = `${width}px`;
260
+ el.style.height = `${height}px`;
261
+ }
262
+ this.app.send(CreateElementNode(id, parentID, index, el.tagName, isSVGElement(node)));
263
+ }
264
+ for (let i = 0; i < el.attributes.length; i++) {
265
+ const attr = el.attributes[i];
266
+ this.sendNodeAttribute(id, el, attr.nodeName, attr.value);
267
+ }
268
+ }
269
+ else if (isTextNode(node)) {
270
+ // for text node id != 0, hence parentID !== undefined and parent is Element
271
+ this.app.send(CreateTextNode(id, parentID, index));
272
+ this.sendNodeData(id, parent, node.data);
273
+ }
274
+ return true;
275
+ }
276
+ if (recentsType === RecentsType.Removed && parentID !== undefined) {
277
+ this.app.send(MoveNode(id, parentID, index));
278
+ }
279
+ const attr = this.attributesMap.get(id);
280
+ if (attr !== undefined) {
281
+ if (!isElementNode(node)) {
282
+ throw 'commitNode: node is not an element';
283
+ }
284
+ for (const name of attr) {
285
+ this.sendNodeAttribute(id, node, name, node.getAttribute(name));
286
+ }
287
+ }
288
+ if (this.textSet.has(id)) {
289
+ if (!isTextNode(node)) {
290
+ throw 'commitNode: node is not a text';
291
+ }
292
+ // for text node id != 0, hence parent is Element
293
+ this.sendNodeData(id, parent, node.data);
294
+ }
295
+ return true;
296
+ }
297
+ commitNode(id) {
298
+ const node = this.app.nodes.getNode(id);
299
+ if (node === undefined) {
300
+ return false;
301
+ }
302
+ const cmt = this.commited[id];
303
+ if (cmt !== undefined) {
304
+ return cmt;
305
+ }
306
+ return (this.commited[id] = this._commitNode(id, node));
307
+ }
308
+ commitNodes(isStart = false) {
309
+ let node;
310
+ this.recents.forEach((type, id) => {
311
+ this.commitNode(id);
312
+ if (type === RecentsType.New && (node = this.app.nodes.getNode(id))) {
313
+ this.app.nodes.callNodeCallbacks(node, isStart);
314
+ }
315
+ });
316
+ this.clear();
317
+ }
318
+ // ISSSUE (nodeToBinde should be the same as node in all cases. Look at the comment about 0-node at the beginning of the file.)
319
+ // TODO: use one observer instance for all iframes/shadowRoots (composition instiad of inheritance)
320
+ observeRoot(node, beforeCommit, nodeToBind = node) {
321
+ this.observer.observe(node, {
322
+ childList: true,
323
+ attributes: true,
324
+ characterData: true,
325
+ subtree: true,
326
+ attributeOldValue: false,
327
+ characterDataOldValue: false,
328
+ });
329
+ this.bindTree(nodeToBind);
330
+ beforeCommit(this.app.nodes.getID(node));
331
+ this.commitNodes(true);
332
+ }
333
+ disconnect() {
334
+ this.observer.disconnect();
335
+ this.clear();
336
+ }
337
+ }
@@ -0,0 +1,4 @@
1
+ import Observer from './observer.js';
2
+ export default class ShadowRootObserver extends Observer {
3
+ observe(el: Element): void;
4
+ }
@@ -0,0 +1,18 @@
1
+ import Observer from './observer.js';
2
+ import { CreateIFrameDocument } from '../messages.gen.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,24 @@
1
+ import Observer from './observer.js';
2
+ import { Offset } from './iframe_offsets.js';
3
+ import App from '../index.js';
4
+ export interface Options {
5
+ captureIFrames: boolean;
6
+ }
7
+ type Context = Window & typeof globalThis;
8
+ type ContextCallback = (context: Context) => void;
9
+ export default class TopObserver extends Observer {
10
+ private readonly options;
11
+ private readonly iframeOffsets;
12
+ constructor(app: App, options: Partial<Options>);
13
+ private readonly contextCallbacks;
14
+ private readonly contextsSet;
15
+ attachContextCallback(cb: ContextCallback): void;
16
+ getDocumentOffset(doc: Document): Offset;
17
+ private iframeObservers;
18
+ private handleIframe;
19
+ private shadowRootObservers;
20
+ private handleShadowRoot;
21
+ observe(): void;
22
+ disconnect(): void;
23
+ }
24
+ export {};
@@ -0,0 +1,110 @@
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 IFrameOffsets from './iframe_offsets.js';
6
+ import { CreateDocument } from '../messages.gen.js';
7
+ import { IN_BROWSER, hasOpenreplayAttribute } from '../../utils.js';
8
+ const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () => new ShadowRoot();
9
+ export default class TopObserver extends Observer {
10
+ constructor(app, options) {
11
+ super(app, true);
12
+ this.iframeOffsets = new IFrameOffsets();
13
+ this.contextCallbacks = [];
14
+ // Attached once per Tracker instance
15
+ this.contextsSet = new Set();
16
+ this.iframeObservers = [];
17
+ this.shadowRootObservers = [];
18
+ this.options = Object.assign({
19
+ captureIFrames: true,
20
+ }, options);
21
+ // IFrames
22
+ this.app.nodes.attachNodeCallback((node) => {
23
+ if (hasTag(node, 'iframe') &&
24
+ ((this.options.captureIFrames && !hasOpenreplayAttribute(node, 'obscured')) ||
25
+ hasOpenreplayAttribute(node, 'capture'))) {
26
+ this.handleIframe(node);
27
+ }
28
+ });
29
+ // ShadowDOM
30
+ this.app.nodes.attachNodeCallback((node) => {
31
+ if (isElementNode(node) && node.shadowRoot !== null) {
32
+ this.handleShadowRoot(node.shadowRoot);
33
+ }
34
+ });
35
+ }
36
+ attachContextCallback(cb) {
37
+ this.contextCallbacks.push(cb);
38
+ }
39
+ getDocumentOffset(doc) {
40
+ return this.iframeOffsets.getDocumentOffset(doc);
41
+ }
42
+ handleIframe(iframe) {
43
+ let doc = null;
44
+ // setTimeout is required. Otherwise some event listeners (scroll, mousemove) applied in modules
45
+ // do not work on the iframe document when it 've been loaded dynamically ((why?))
46
+ const handle = this.app.safe(() => setTimeout(() => {
47
+ const id = this.app.nodes.getID(iframe);
48
+ if (id === undefined) {
49
+ //log
50
+ return;
51
+ }
52
+ const currentWin = iframe.contentWindow;
53
+ const currentDoc = iframe.contentDocument;
54
+ if (currentDoc && currentDoc !== doc) {
55
+ const observer = new IFrameObserver(this.app);
56
+ this.iframeObservers.push(observer);
57
+ observer.observe(iframe); // TODO: call unregisterNode for the previous doc if present (incapsulate: one iframe - one observer)
58
+ doc = currentDoc;
59
+ this.iframeOffsets.observe(iframe);
60
+ }
61
+ if (currentWin &&
62
+ // Sometimes currentWin.window is null (not in specification). Such window object is not functional
63
+ currentWin === currentWin.window &&
64
+ !this.contextsSet.has(currentWin) // for each context callbacks called once per Tracker (TopObserver) instance
65
+ //TODO: more explicit logic
66
+ ) {
67
+ this.contextsSet.add(currentWin);
68
+ //@ts-ignore https://github.com/microsoft/TypeScript/issues/41684
69
+ this.contextCallbacks.forEach((cb) => cb(currentWin));
70
+ }
71
+ }, 0));
72
+ iframe.addEventListener('load', handle); // why app.attachEventListener not working?
73
+ handle();
74
+ }
75
+ handleShadowRoot(shRoot) {
76
+ const observer = new ShadowRootObserver(this.app);
77
+ this.shadowRootObservers.push(observer);
78
+ observer.observe(shRoot.host);
79
+ }
80
+ observe() {
81
+ // Protection from several subsequent calls?
82
+ const observer = this;
83
+ Element.prototype.attachShadow = function () {
84
+ // eslint-disable-next-line
85
+ const shadow = attachShadowNativeFn.apply(this, arguments);
86
+ observer.handleShadowRoot(shadow);
87
+ return shadow;
88
+ };
89
+ // Can observe documentElement (<html>) here, because it is not supposed to be changing.
90
+ // However, it is possible in some exotic cases and may cause an ignorance of the newly created <html>
91
+ // In this case context.document have to be observed, but this will cause
92
+ // the change in the re-player behaviour caused by CreateDocument message:
93
+ // the 0-node ("fRoot") will become #document rather than documentElement as it is now.
94
+ // Alternatively - observe(#document) then bindNode(documentElement)
95
+ this.observeRoot(window.document, () => {
96
+ this.app.send(CreateDocument());
97
+ // it has no node_id here
98
+ this.app.nodes.callNodeCallbacks(document, true);
99
+ }, window.document.documentElement);
100
+ }
101
+ disconnect() {
102
+ this.iframeOffsets.clear();
103
+ Element.prototype.attachShadow = attachShadowNativeFn;
104
+ this.iframeObservers.forEach((o) => o.disconnect());
105
+ this.iframeObservers = [];
106
+ this.shadowRootObservers.forEach((o) => o.disconnect());
107
+ this.shadowRootObservers = [];
108
+ super.disconnect();
109
+ }
110
+ }
@@ -0,0 +1,24 @@
1
+ import type App from './index.js';
2
+ export declare enum SanitizeLevel {
3
+ Plain = 0,
4
+ Obscured = 1,
5
+ Hidden = 2
6
+ }
7
+ export interface Options {
8
+ obscureTextEmails: boolean;
9
+ obscureTextNumbers: boolean;
10
+ domSanitizer?: (node: Element) => SanitizeLevel;
11
+ }
12
+ export default class Sanitizer {
13
+ private readonly app;
14
+ private readonly obscured;
15
+ private readonly hidden;
16
+ private readonly options;
17
+ constructor(app: App, options: Partial<Options>);
18
+ handleNode(id: number, parentID: number, node: Node): void;
19
+ sanitize(id: number, data: string): string;
20
+ isObscured(id: number): boolean;
21
+ isHidden(id: number): boolean;
22
+ getInnerTextSecure(el: HTMLElement): string;
23
+ clear(): void;
24
+ }
@@ -0,0 +1,72 @@
1
+ import { stars, hasOpenreplayAttribute } from '../utils.js';
2
+ import { isElementNode } from './guards.js';
3
+ export var SanitizeLevel;
4
+ (function (SanitizeLevel) {
5
+ SanitizeLevel[SanitizeLevel["Plain"] = 0] = "Plain";
6
+ SanitizeLevel[SanitizeLevel["Obscured"] = 1] = "Obscured";
7
+ SanitizeLevel[SanitizeLevel["Hidden"] = 2] = "Hidden";
8
+ })(SanitizeLevel || (SanitizeLevel = {}));
9
+ export default class Sanitizer {
10
+ constructor(app, options) {
11
+ this.app = app;
12
+ this.obscured = new Set();
13
+ this.hidden = new Set();
14
+ this.options = Object.assign({
15
+ obscureTextEmails: true,
16
+ obscureTextNumbers: false,
17
+ }, options);
18
+ }
19
+ handleNode(id, parentID, node) {
20
+ if (this.obscured.has(parentID) ||
21
+ (isElementNode(node) &&
22
+ (hasOpenreplayAttribute(node, 'masked') || hasOpenreplayAttribute(node, 'obscured')))) {
23
+ this.obscured.add(id);
24
+ }
25
+ if (this.hidden.has(parentID) ||
26
+ (isElementNode(node) &&
27
+ (hasOpenreplayAttribute(node, 'htmlmasked') || hasOpenreplayAttribute(node, 'hidden')))) {
28
+ this.hidden.add(id);
29
+ }
30
+ if (this.options.domSanitizer !== undefined && isElementNode(node)) {
31
+ const sanitizeLevel = this.options.domSanitizer(node);
32
+ if (sanitizeLevel === SanitizeLevel.Obscured) {
33
+ this.obscured.add(id);
34
+ }
35
+ if (sanitizeLevel === SanitizeLevel.Hidden) {
36
+ this.hidden.add(id);
37
+ }
38
+ }
39
+ }
40
+ sanitize(id, data) {
41
+ if (this.obscured.has(id)) {
42
+ // TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
43
+ return data
44
+ .trim()
45
+ .replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█');
46
+ }
47
+ if (this.options.obscureTextNumbers) {
48
+ data = data.replace(/\d/g, '0');
49
+ }
50
+ if (this.options.obscureTextEmails) {
51
+ data = data.replace(/([^\s]+)@([^\s]+)\.([^\s]+)/g, (...f) => stars(f[1]) + '@' + stars(f[2]) + '.' + stars(f[3]));
52
+ }
53
+ return data;
54
+ }
55
+ isObscured(id) {
56
+ return this.obscured.has(id);
57
+ }
58
+ isHidden(id) {
59
+ return this.hidden.has(id);
60
+ }
61
+ getInnerTextSecure(el) {
62
+ const id = this.app.nodes.getID(el);
63
+ if (!id) {
64
+ return '';
65
+ }
66
+ return this.sanitize(id, el.innerText);
67
+ }
68
+ clear() {
69
+ this.obscured.clear();
70
+ this.hidden.clear();
71
+ }
72
+ }
@@ -0,0 +1,38 @@
1
+ import type App from './index.js';
2
+ interface SessionInfo {
3
+ sessionID: string | undefined;
4
+ metadata: Record<string, string>;
5
+ userID: string | null;
6
+ timestamp: number;
7
+ projectID?: string;
8
+ }
9
+ type OnUpdateCallback = (i: Partial<SessionInfo>) => void;
10
+ export type Options = {
11
+ session_token_key: string;
12
+ session_pageno_key: string;
13
+ };
14
+ export default class Session {
15
+ private readonly app;
16
+ private readonly options;
17
+ private metadata;
18
+ private userID;
19
+ private sessionID;
20
+ private readonly callbacks;
21
+ private timestamp;
22
+ private projectID;
23
+ constructor(app: App, options: Options);
24
+ attachUpdateCallback(cb: OnUpdateCallback): void;
25
+ private handleUpdate;
26
+ assign(newInfo: Partial<SessionInfo>): void;
27
+ setMetadata(key: string, value: string): void;
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;
35
+ getInfo(): SessionInfo;
36
+ reset(): void;
37
+ }
38
+ export {};