@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.
Files changed (45) hide show
  1. package/cjs/app/context.d.ts +18 -0
  2. package/cjs/app/context.js +48 -0
  3. package/cjs/app/index.d.ts +11 -6
  4. package/cjs/app/index.js +33 -22
  5. package/cjs/app/observer/iframe_observer.d.ts +4 -0
  6. package/cjs/app/observer/iframe_observer.js +22 -0
  7. package/cjs/app/observer/observer.d.ts +25 -0
  8. package/cjs/app/{observer.js → observer/observer.js} +82 -170
  9. package/cjs/app/observer/shadow_root_observer.d.ts +4 -0
  10. package/cjs/app/observer/shadow_root_observer.js +21 -0
  11. package/cjs/app/observer/top_observer.d.ts +15 -0
  12. package/cjs/app/observer/top_observer.js +84 -0
  13. package/cjs/app/sanitizer.d.ts +16 -0
  14. package/cjs/app/sanitizer.js +46 -0
  15. package/cjs/index.d.ts +1 -0
  16. package/cjs/index.js +7 -1
  17. package/cjs/modules/exception.js +8 -1
  18. package/cjs/modules/img.js +15 -1
  19. package/cjs/modules/input.d.ts +3 -1
  20. package/cjs/modules/input.js +6 -3
  21. package/cjs/modules/mouse.js +1 -1
  22. package/lib/app/context.d.ts +18 -0
  23. package/lib/app/context.js +43 -0
  24. package/lib/app/index.d.ts +11 -6
  25. package/lib/app/index.js +33 -22
  26. package/lib/app/observer/iframe_observer.d.ts +4 -0
  27. package/lib/app/observer/iframe_observer.js +19 -0
  28. package/lib/app/observer/observer.d.ts +25 -0
  29. package/lib/app/{observer.js → observer/observer.js} +82 -170
  30. package/lib/app/observer/shadow_root_observer.d.ts +4 -0
  31. package/lib/app/observer/shadow_root_observer.js +18 -0
  32. package/lib/app/observer/top_observer.d.ts +15 -0
  33. package/lib/app/observer/top_observer.js +81 -0
  34. package/lib/app/sanitizer.d.ts +16 -0
  35. package/lib/app/sanitizer.js +44 -1
  36. package/lib/index.d.ts +1 -0
  37. package/lib/index.js +7 -1
  38. package/lib/modules/exception.js +8 -1
  39. package/lib/modules/img.js +16 -2
  40. package/lib/modules/input.d.ts +3 -1
  41. package/lib/modules/input.js +6 -3
  42. package/lib/modules/mouse.js +1 -1
  43. package/package.json +1 -1
  44. package/cjs/app/observer.d.ts +0 -47
  45. package/lib/app/observer.d.ts +0 -47
@@ -1,40 +1,52 @@
1
- import { stars, hasOpenreplayAttribute } from "../utils.js";
2
- import { CreateDocument, CreateElementNode, CreateTextNode, SetNodeData, SetCSSDataURLBased, SetNodeAttribute, SetNodeAttributeURLBased, RemoveNodeAttribute, MoveNode, RemoveNode, CreateIFrameDocument, } from "../messages/index.js";
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, options, context = window) {
35
+ constructor(app, context = window) {
8
36
  this.app = app;
9
- this.options = options;
10
37
  this.context = context;
11
- this.iframeObservers = [];
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
- // Special case
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
- this.isInstance(node, HTMLInputElement) &&
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' && this.isInstance(node, HTMLLinkElement)) {
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 (this.isInstance(parentElement, HTMLStyleElement) || this.isInstance(parentElement, SVGStyleElement)) {
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.checkObscure(id, data);
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 (this.isIgnored(node)) {
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) => this.isIgnored(node) || this.app.nodes.getID(node) !== undefined
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
- if (this.isInstance(node, HTMLHtmlElement)) {
230
- this.indexes[id] = 0;
231
- }
232
- else {
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
- if (this.textMasked.has(parentID) ||
247
- (this.isInstance(node, Element) && hasOpenreplayAttribute(node, 'masked'))) {
248
- this.textMasked.add(id);
249
- }
250
- let sibling = node.previousSibling;
251
- while (sibling !== null) {
252
- const siblingID = this.app.nodes.getID(sibling);
253
- if (siblingID !== undefined) {
254
- this.commitNode(siblingID);
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 (this.isInstance(node, Element)) {
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 (this.isInstance(node, Text)) {
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 (!this.isInstance(node, Element)) {
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 (!this.isInstance(node, Text)) {
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
- handleIframe(iframe) {
333
- let context = null;
334
- const handle = this.app.safe(() => {
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.app.send(new CreateDocument());
383
- this.bindTree(this.context.document.documentElement);
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,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/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
+ }
@@ -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
+ }
@@ -1 +1,44 @@
1
- "use strict";
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
@@ -37,4 +37,5 @@ export default class API {
37
37
  event(key: string, payload: any, issue?: boolean): void;
38
38
  issue(key: string, payload: any): void;
39
39
  handleError: (e: Error | ErrorEvent | PromiseRejectionEvent) => void;
40
+ resetNextPageSession(flag: boolean): void;
40
41
  }
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.13',
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
  }
@@ -37,7 +37,14 @@ export function getExceptionMessageFromEvent(e) {
37
37
  return getExceptionMessage(e.reason, []);
38
38
  }
39
39
  else {
40
- return new JSException('Unhandled Promise Rejection', String(e.reason), '[]');
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;
@@ -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 < 1e5) {
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
  });
@@ -1,5 +1,6 @@
1
1
  import App from "../app/index.js";
2
- export declare function getInputLabel(node: HTMLInputElement): string;
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 {};