@openreplay/tracker 3.5.16 → 3.6.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 (128) hide show
  1. package/.eslintignore +8 -0
  2. package/.prettierignore +1 -0
  3. package/LICENSE +1 -1
  4. package/cjs/app/guards.d.ts +2 -1
  5. package/cjs/app/guards.js +6 -3
  6. package/cjs/app/index.d.ts +28 -23
  7. package/cjs/app/index.js +107 -86
  8. package/cjs/app/logger.js +6 -3
  9. package/cjs/app/messages.d.ts +52 -0
  10. package/cjs/app/messages.gen.d.ts +57 -0
  11. package/cjs/app/messages.gen.js +493 -0
  12. package/cjs/app/messages.js +234 -0
  13. package/cjs/app/nodes.d.ts +1 -1
  14. package/cjs/app/nodes.js +2 -0
  15. package/cjs/app/observer/iframe_observer.d.ts +1 -1
  16. package/cjs/app/observer/iframe_observer.js +3 -3
  17. package/cjs/app/observer/observer.d.ts +2 -3
  18. package/cjs/app/observer/observer.js +50 -52
  19. package/cjs/app/observer/shadow_root_observer.d.ts +1 -1
  20. package/cjs/app/observer/shadow_root_observer.js +3 -3
  21. package/cjs/app/observer/top_observer.d.ts +13 -2
  22. package/cjs/app/observer/top_observer.js +58 -23
  23. package/cjs/app/sanitizer.d.ts +1 -1
  24. package/cjs/app/sanitizer.js +5 -5
  25. package/cjs/app/session.d.ts +20 -2
  26. package/cjs/app/session.js +65 -6
  27. package/cjs/app/ticker.d.ts +1 -1
  28. package/cjs/common/{webworker.d.ts → interaction.d.ts} +5 -5
  29. package/cjs/common/{types.js → interaction.js} +0 -0
  30. package/cjs/common/messages.gen.d.ts +382 -0
  31. package/cjs/common/{webworker.js → messages.gen.js} +1 -0
  32. package/cjs/index.d.ts +10 -9
  33. package/cjs/index.js +47 -36
  34. package/cjs/modules/adoptedStyleSheets.d.ts +2 -0
  35. package/cjs/modules/adoptedStyleSheets.js +127 -0
  36. package/cjs/modules/connection.d.ts +1 -1
  37. package/cjs/modules/connection.js +2 -2
  38. package/cjs/modules/console.d.ts +1 -1
  39. package/cjs/modules/console.js +7 -21
  40. package/cjs/modules/cssrules.d.ts +1 -1
  41. package/cjs/modules/cssrules.js +18 -14
  42. package/cjs/modules/exception.d.ts +3 -3
  43. package/cjs/modules/exception.js +23 -18
  44. package/cjs/modules/img.d.ts +1 -1
  45. package/cjs/modules/img.js +39 -26
  46. package/cjs/modules/input.d.ts +1 -1
  47. package/cjs/modules/input.js +21 -21
  48. package/cjs/modules/mouse.d.ts +1 -1
  49. package/cjs/modules/mouse.js +50 -43
  50. package/cjs/modules/performance.d.ts +1 -1
  51. package/cjs/modules/performance.js +2 -2
  52. package/cjs/modules/scroll.d.ts +1 -1
  53. package/cjs/modules/scroll.js +16 -7
  54. package/cjs/modules/timing.d.ts +1 -1
  55. package/cjs/modules/timing.js +14 -26
  56. package/cjs/modules/viewport.d.ts +1 -1
  57. package/cjs/modules/viewport.js +4 -4
  58. package/cjs/utils.js +7 -7
  59. package/cjs/vendors/finder/finder.js +53 -48
  60. package/lib/app/guards.d.ts +2 -1
  61. package/lib/app/guards.js +4 -2
  62. package/lib/app/index.d.ts +28 -23
  63. package/lib/app/index.js +115 -94
  64. package/lib/app/logger.js +6 -3
  65. package/lib/app/messages.d.ts +52 -0
  66. package/lib/app/messages.gen.d.ts +57 -0
  67. package/lib/app/messages.gen.js +434 -0
  68. package/lib/app/messages.js +181 -0
  69. package/lib/app/nodes.d.ts +1 -1
  70. package/lib/app/nodes.js +2 -0
  71. package/lib/app/observer/iframe_observer.d.ts +1 -1
  72. package/lib/app/observer/iframe_observer.js +3 -3
  73. package/lib/app/observer/observer.d.ts +2 -3
  74. package/lib/app/observer/observer.js +51 -53
  75. package/lib/app/observer/shadow_root_observer.d.ts +1 -1
  76. package/lib/app/observer/shadow_root_observer.js +3 -3
  77. package/lib/app/observer/top_observer.d.ts +13 -2
  78. package/lib/app/observer/top_observer.js +62 -27
  79. package/lib/app/sanitizer.d.ts +1 -1
  80. package/lib/app/sanitizer.js +7 -7
  81. package/lib/app/session.d.ts +20 -2
  82. package/lib/app/session.js +65 -6
  83. package/lib/app/ticker.d.ts +1 -1
  84. package/lib/common/{webworker.d.ts → interaction.d.ts} +5 -5
  85. package/lib/common/{types.js → interaction.js} +0 -0
  86. package/lib/common/messages.gen.d.ts +382 -0
  87. package/lib/common/messages.gen.js +2 -0
  88. package/lib/common/tsconfig.tsbuildinfo +1 -1
  89. package/lib/index.d.ts +10 -9
  90. package/lib/index.js +60 -49
  91. package/lib/modules/adoptedStyleSheets.d.ts +2 -0
  92. package/lib/modules/adoptedStyleSheets.js +124 -0
  93. package/lib/modules/connection.d.ts +1 -1
  94. package/lib/modules/connection.js +2 -2
  95. package/lib/modules/console.d.ts +1 -1
  96. package/lib/modules/console.js +8 -22
  97. package/lib/modules/cssrules.d.ts +1 -1
  98. package/lib/modules/cssrules.js +19 -15
  99. package/lib/modules/exception.d.ts +3 -3
  100. package/lib/modules/exception.js +23 -18
  101. package/lib/modules/img.d.ts +1 -1
  102. package/lib/modules/img.js +41 -28
  103. package/lib/modules/input.d.ts +1 -1
  104. package/lib/modules/input.js +23 -23
  105. package/lib/modules/mouse.d.ts +1 -1
  106. package/lib/modules/mouse.js +53 -46
  107. package/lib/modules/performance.d.ts +1 -1
  108. package/lib/modules/performance.js +3 -3
  109. package/lib/modules/scroll.d.ts +1 -1
  110. package/lib/modules/scroll.js +17 -8
  111. package/lib/modules/timing.d.ts +1 -1
  112. package/lib/modules/timing.js +16 -28
  113. package/lib/modules/viewport.d.ts +1 -1
  114. package/lib/modules/viewport.js +4 -4
  115. package/lib/utils.js +7 -7
  116. package/lib/vendors/finder/finder.js +53 -48
  117. package/package.json +27 -10
  118. package/cjs/common/messages.d.ts +0 -444
  119. package/cjs/common/messages.js +0 -794
  120. package/cjs/common/types.d.ts +0 -9
  121. package/cjs/modules/longtasks.d.ts +0 -2
  122. package/cjs/modules/longtasks.js +0 -26
  123. package/lib/common/messages.d.ts +0 -444
  124. package/lib/common/messages.js +0 -790
  125. package/lib/common/types.d.ts +0 -9
  126. package/lib/common/webworker.js +0 -1
  127. package/lib/modules/longtasks.d.ts +0 -2
  128. package/lib/modules/longtasks.js +0 -23
@@ -1,5 +1,5 @@
1
- import { RemoveNodeAttribute, SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, SetNodeData, CreateTextNode, CreateElementNode, MoveNode, RemoveNode, } from "../../common/messages.js";
2
- import { isRootNode, isTextNode, isElementNode, isSVGElement, hasTag, } from "../guards.js";
1
+ import { RemoveNodeAttribute, SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, SetNodeData, CreateTextNode, CreateElementNode, MoveNode, RemoveNode, } from '../messages.gen.js';
2
+ import { isRootNode, isTextNode, isElementNode, isSVGElement, hasTag } from '../guards.js';
3
3
  function isIgnored(node) {
4
4
  if (isTextNode(node)) {
5
5
  return false;
@@ -11,13 +11,9 @@ function isIgnored(node) {
11
11
  if (tag === 'LINK') {
12
12
  const rel = node.getAttribute('rel');
13
13
  const as = node.getAttribute('as');
14
- return !((rel === null || rel === void 0 ? void 0 : rel.includes('stylesheet')) || as === "style" || as === "font");
14
+ return !((rel === null || rel === void 0 ? void 0 : rel.includes('stylesheet')) || as === 'style' || as === 'font');
15
15
  }
16
- return (tag === 'SCRIPT' ||
17
- tag === 'NOSCRIPT' ||
18
- tag === 'META' ||
19
- tag === 'TITLE' ||
20
- tag === 'BASE');
16
+ return (tag === 'SCRIPT' || tag === 'NOSCRIPT' || tag === 'META' || tag === 'TITLE' || tag === 'BASE');
21
17
  }
22
18
  function isObservable(node) {
23
19
  if (isRootNode(node)) {
@@ -30,17 +26,11 @@ function isObservable(node) {
30
26
  - fix unbinding logic + send all removals first (ensure sequence is correct)
31
27
  - use document as a 0-node in the upper context (should be updated in player at first)
32
28
  */
33
- /*
34
- Nikita:
35
- - rn we only send unbind event for parent (all child nodes will be cut in the live replay anyways)
36
- to prevent sending 1k+ unbinds for child nodes and making replay file bigger than it should be
37
- */
38
29
  var RecentsType;
39
30
  (function (RecentsType) {
40
31
  RecentsType[RecentsType["New"] = 0] = "New";
41
32
  RecentsType[RecentsType["Removed"] = 1] = "Removed";
42
33
  RecentsType[RecentsType["Changed"] = 2] = "Changed";
43
- RecentsType[RecentsType["RemovedChild"] = 3] = "RemovedChild";
44
34
  })(RecentsType || (RecentsType = {}));
45
35
  export default class Observer {
46
36
  constructor(app, isTopContext = false) {
@@ -52,7 +42,8 @@ export default class Observer {
52
42
  this.attributesMap = new Map();
53
43
  this.textSet = new Set();
54
44
  this.observer = new MutationObserver(this.app.safe((mutations) => {
55
- for (const mutation of mutations) { // mutations order is sequential
45
+ for (const mutation of mutations) {
46
+ // mutations order is sequential
56
47
  const target = mutation.target;
57
48
  const type = mutation.type;
58
49
  if (!isObservable(target)) {
@@ -60,7 +51,10 @@ export default class Observer {
60
51
  }
61
52
  if (type === 'childList') {
62
53
  for (let i = 0; i < mutation.removedNodes.length; i++) {
63
- this.bindTree(mutation.removedNodes[i], true);
54
+ // Should be the same as bindTree(mutation.removedNodes[i]), but logic needs to be be untied
55
+ if (isObservable(mutation.removedNodes[i])) {
56
+ this.bindNode(mutation.removedNodes[i]);
57
+ }
64
58
  }
65
59
  for (let i = 0; i < mutation.addedNodes.length; i++) {
66
60
  this.bindTree(mutation.addedNodes[i]);
@@ -81,7 +75,7 @@ export default class Observer {
81
75
  }
82
76
  let attr = this.attributesMap.get(id);
83
77
  if (attr === undefined) {
84
- this.attributesMap.set(id, attr = new Set());
78
+ this.attributesMap.set(id, (attr = new Set()));
85
79
  }
86
80
  attr.add(name);
87
81
  continue;
@@ -107,16 +101,16 @@ export default class Observer {
107
101
  name = name.substr(6);
108
102
  }
109
103
  if (value === null) {
110
- this.app.send(new RemoveNodeAttribute(id, name));
104
+ this.app.send(RemoveNodeAttribute(id, name));
111
105
  }
112
106
  else if (name === 'href') {
113
107
  if (value.length > 1e5) {
114
108
  value = '';
115
109
  }
116
- this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
110
+ this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
117
111
  }
118
112
  else {
119
- this.app.send(new SetNodeAttribute(id, name, value));
113
+ this.app.send(SetNodeAttribute(id, name, value));
120
114
  }
121
115
  return;
122
116
  }
@@ -129,72 +123,75 @@ export default class Observer {
129
123
  return;
130
124
  }
131
125
  if (name === 'value' &&
132
- hasTag(node, "INPUT") &&
126
+ hasTag(node, 'INPUT') &&
133
127
  node.type !== 'button' &&
134
128
  node.type !== 'reset' &&
135
129
  node.type !== 'submit') {
136
130
  return;
137
131
  }
138
132
  if (value === null) {
139
- this.app.send(new RemoveNodeAttribute(id, name));
133
+ this.app.send(RemoveNodeAttribute(id, name));
140
134
  return;
141
135
  }
142
- if (name === 'style' || name === 'href' && hasTag(node, "LINK")) {
143
- this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
136
+ if (name === 'style' || (name === 'href' && hasTag(node, 'LINK'))) {
137
+ this.app.send(SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
144
138
  return;
145
139
  }
146
140
  if (name === 'href' || value.length > 1e5) {
147
141
  value = '';
148
142
  }
149
- this.app.send(new SetNodeAttribute(id, name, value));
143
+ this.app.send(SetNodeAttribute(id, name, value));
150
144
  }
151
145
  sendNodeData(id, parentElement, data) {
152
- if (hasTag(parentElement, "STYLE") || hasTag(parentElement, "style")) {
153
- this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref()));
146
+ if (hasTag(parentElement, 'STYLE') || hasTag(parentElement, 'style')) {
147
+ this.app.send(SetCSSDataURLBased(id, data, this.app.getBaseHref()));
154
148
  return;
155
149
  }
156
150
  data = this.app.sanitizer.sanitize(id, data);
157
- this.app.send(new SetNodeData(id, data));
151
+ this.app.send(SetNodeData(id, data));
158
152
  }
159
153
  bindNode(node) {
160
154
  const [id, isNew] = this.app.nodes.registerNode(node);
161
155
  if (isNew) {
162
156
  this.recents.set(id, RecentsType.New);
163
157
  }
164
- else if (this.recents.get(id) !== RecentsType.New) { // can we do just `else` here?
158
+ else if (this.recents.get(id) !== RecentsType.New) {
165
159
  this.recents.set(id, RecentsType.Removed);
166
160
  }
167
161
  }
168
- unbindChildNode(node) {
169
- const [id] = this.app.nodes.registerNode(node);
170
- this.recents.set(id, RecentsType.RemovedChild);
171
- }
172
- bindTree(node, isChildUnbinding = false) {
162
+ bindTree(node) {
173
163
  if (!isObservable(node)) {
174
164
  return;
175
165
  }
176
166
  this.bindNode(node);
177
167
  const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
178
- acceptNode: (node) => isIgnored(node)
179
- || (this.app.nodes.getID(node) !== undefined && !isChildUnbinding)
168
+ acceptNode: (node) => isIgnored(node) || this.app.nodes.getID(node) !== undefined
180
169
  ? NodeFilter.FILTER_REJECT
181
170
  : NodeFilter.FILTER_ACCEPT,
182
171
  },
183
172
  // @ts-ignore
184
173
  false);
185
174
  while (walker.nextNode()) {
186
- if (isChildUnbinding) {
187
- this.unbindChildNode(walker.currentNode);
188
- }
189
- else {
190
- this.bindNode(walker.currentNode);
191
- }
175
+ this.bindNode(walker.currentNode);
192
176
  }
193
177
  }
194
- unbindNode(node) {
178
+ unbindTree(node) {
195
179
  const id = this.app.nodes.unregisterNode(node);
196
180
  if (id !== undefined && this.recents.get(id) === RecentsType.Removed) {
197
- this.app.send(new RemoveNode(id));
181
+ // Sending RemoveNode only for parent to maintain
182
+ this.app.send(RemoveNode(id));
183
+ // Unregistering all the children in order to clear the memory
184
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, {
185
+ acceptNode: (node) => isIgnored(node) || this.app.nodes.getID(node) === undefined
186
+ ? NodeFilter.FILTER_REJECT
187
+ : NodeFilter.FILTER_ACCEPT,
188
+ },
189
+ // @ts-ignore
190
+ false);
191
+ while (walker.nextNode()) {
192
+ this.app.nodes.unregisterNode(walker.currentNode);
193
+ }
194
+ // MBTODO: count and send RemovedNodesCount (for the page crash detection in heuristics)
198
195
  }
199
196
  }
200
197
  // A top-consumption function on the infinite lists test. (~1% of performance resources)
@@ -207,20 +204,20 @@ export default class Observer {
207
204
  // Disable parent check for the upper context HTMLHtmlElement, because it is root there... (before)
208
205
  // TODO: get rid of "special" cases (there is an issue with CreateDocument altered behaviour though)
209
206
  // TODO: Clean the logic (though now it workd fine)
210
- if (!hasTag(node, "HTML") || !this.isTopContext) {
207
+ if (!hasTag(node, 'HTML') || !this.isTopContext) {
211
208
  if (parent === null) {
212
209
  // Sometimes one observation contains attribute mutations for the removimg node, which gets ignored here.
213
- // That shouldn't affect the visual rendering ( should it? )
214
- this.unbindNode(node);
210
+ // That shouldn't affect the visual rendering ( should it? maybe when transition applied? )
211
+ this.unbindTree(node);
215
212
  return false;
216
213
  }
217
214
  parentID = this.app.nodes.getID(parent);
218
215
  if (parentID === undefined) {
219
- this.unbindNode(node);
216
+ this.unbindTree(node);
220
217
  return false;
221
218
  }
222
219
  if (!this.commitNode(parentID)) {
223
- this.unbindNode(node);
220
+ this.unbindTree(node);
224
221
  return false;
225
222
  }
226
223
  this.app.sanitizer.handleNode(id, parentID, node);
@@ -259,7 +256,7 @@ export default class Observer {
259
256
  el.style.width = width + 'px';
260
257
  el.style.height = height + 'px';
261
258
  }
262
- this.app.send(new CreateElementNode(id, parentID, index, el.tagName, isSVGElement(node)));
259
+ this.app.send(CreateElementNode(id, parentID, index, el.tagName, isSVGElement(node)));
263
260
  }
264
261
  for (let i = 0; i < el.attributes.length; i++) {
265
262
  const attr = el.attributes[i];
@@ -268,13 +265,13 @@ export default class Observer {
268
265
  }
269
266
  else if (isTextNode(node)) {
270
267
  // for text node id != 0, hence parentID !== undefined and parent is Element
271
- this.app.send(new CreateTextNode(id, parentID, index));
268
+ this.app.send(CreateTextNode(id, parentID, index));
272
269
  this.sendNodeData(id, parent, node.data);
273
270
  }
274
271
  return true;
275
272
  }
276
273
  if (recentsType === RecentsType.Removed && parentID !== undefined) {
277
- this.app.send(new MoveNode(id, parentID, index));
274
+ this.app.send(MoveNode(id, parentID, index));
278
275
  }
279
276
  const attr = this.attributesMap.get(id);
280
277
  if (attr !== undefined) {
@@ -315,7 +312,8 @@ export default class Observer {
315
312
  });
316
313
  this.clear();
317
314
  }
318
- // ISSSUE
315
+ // ISSSUE (nodeToBinde should be the same as node. Look at the comment about 0-node at the beginning of the file.)
316
+ // TODO: use one observer instance for all iframes/shadowRoots (composition instiad of inheritance)
319
317
  observeRoot(node, beforeCommit, nodeToBind = node) {
320
318
  this.observer.observe(node, {
321
319
  childList: true,
@@ -1,4 +1,4 @@
1
- import Observer from "./observer.js";
1
+ import Observer from './observer.js';
2
2
  export default class ShadowRootObserver extends Observer {
3
3
  observe(el: Element): void;
4
4
  }
@@ -1,5 +1,5 @@
1
- import Observer from "./observer.js";
2
- import { CreateIFrameDocument } from "../../common/messages.js";
1
+ import Observer from './observer.js';
2
+ import { CreateIFrameDocument } from '../messages.gen.js';
3
3
  export default class ShadowRootObserver extends Observer {
4
4
  observe(el) {
5
5
  const shRoot = el.shadowRoot;
@@ -9,7 +9,7 @@ export default class ShadowRootObserver extends Observer {
9
9
  } // log
10
10
  this.observeRoot(shRoot, (rootID) => {
11
11
  if (rootID === undefined) {
12
- console.log("OpenReplay: Shadow Root was not bound");
12
+ console.log('OpenReplay: Shadow Root was not bound');
13
13
  return;
14
14
  }
15
15
  this.app.send(CreateIFrameDocument(hostID, rootID));
@@ -1,11 +1,21 @@
1
- import Observer from "./observer.js";
2
- import App from "../index.js";
1
+ import Observer from './observer.js';
2
+ import App from '../index.js';
3
3
  export interface Options {
4
4
  captureIFrames: boolean;
5
5
  }
6
+ declare type Context = Window & typeof globalThis;
7
+ declare type ContextCallback = (context: Context) => void;
8
+ declare type Offset = {
9
+ top: number;
10
+ left: number;
11
+ };
6
12
  export default class TopObserver extends Observer {
7
13
  private readonly options;
8
14
  constructor(app: App, options: Partial<Options>);
15
+ private readonly contextCallbacks;
16
+ private readonly contextsSet;
17
+ attachContextCallback(cb: ContextCallback): void;
18
+ getDocumentOffset(doc: Document): Offset;
9
19
  private iframeObservers;
10
20
  private handleIframe;
11
21
  private shadowRootObservers;
@@ -13,3 +23,4 @@ export default class TopObserver extends Observer {
13
23
  observe(): void;
14
24
  disconnect(): void;
15
25
  }
26
+ export {};
@@ -1,52 +1,86 @@
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 { CreateDocument } from "../../common/messages.js";
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 { CreateDocument } from '../messages.gen.js';
6
6
  import { IN_BROWSER, hasOpenreplayAttribute } from '../../utils.js';
7
+ function isPatchedDocument(doc) {
8
+ // @ts-ignore
9
+ return typeof doc.__openreplay__getOffset === 'function';
10
+ }
7
11
  const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () => new ShadowRoot();
8
12
  export default class TopObserver extends Observer {
9
13
  constructor(app, options) {
10
14
  super(app, true);
15
+ this.contextCallbacks = [];
16
+ // Attached once per Tracker instance
17
+ this.contextsSet = new Set();
11
18
  this.iframeObservers = [];
12
19
  this.shadowRootObservers = [];
13
20
  this.options = Object.assign({
14
- captureIFrames: true
21
+ captureIFrames: true,
15
22
  }, options);
16
23
  // IFrames
17
- this.app.nodes.attachNodeCallback(node => {
18
- if (hasTag(node, "IFRAME") &&
19
- ((this.options.captureIFrames && !hasOpenreplayAttribute(node, "obscured"))
20
- || hasOpenreplayAttribute(node, "capture"))) {
24
+ this.app.nodes.attachNodeCallback((node) => {
25
+ if (hasTag(node, 'IFRAME') &&
26
+ ((this.options.captureIFrames && !hasOpenreplayAttribute(node, 'obscured')) ||
27
+ hasOpenreplayAttribute(node, 'capture'))) {
21
28
  this.handleIframe(node);
22
29
  }
23
30
  });
24
31
  // ShadowDOM
25
- this.app.nodes.attachNodeCallback(node => {
32
+ this.app.nodes.attachNodeCallback((node) => {
26
33
  if (isElementNode(node) && node.shadowRoot !== null) {
27
34
  this.handleShadowRoot(node.shadowRoot);
28
35
  }
29
36
  });
30
37
  }
38
+ attachContextCallback(cb) {
39
+ this.contextCallbacks.push(cb);
40
+ }
41
+ // Le truc
42
+ getDocumentOffset(doc) {
43
+ if (isPatchedDocument(doc)) {
44
+ return doc.__openreplay__getOffset();
45
+ }
46
+ return { top: 0, left: 0 };
47
+ }
31
48
  handleIframe(iframe) {
32
49
  let doc = null;
50
+ let win = null;
33
51
  const handle = this.app.safe(() => {
34
52
  const id = this.app.nodes.getID(iframe);
35
53
  if (id === undefined) {
54
+ //log
36
55
  return;
37
- } //log
38
- if (iframe.contentDocument === doc) {
39
- return;
40
- } // How frequently can it happen?
41
- doc = iframe.contentDocument;
42
- if (!doc || !iframe.contentWindow) {
43
- return;
44
56
  }
45
- const observer = new IFrameObserver(this.app);
46
- this.iframeObservers.push(observer);
47
- observer.observe(iframe);
57
+ const currentWin = iframe.contentWindow;
58
+ const currentDoc = iframe.contentDocument;
59
+ if (currentDoc && currentDoc !== doc) {
60
+ const observer = new IFrameObserver(this.app);
61
+ this.iframeObservers.push(observer);
62
+ observer.observe(iframe);
63
+ doc = currentDoc;
64
+ doc.__openreplay__getOffset = () => {
65
+ const { top, left } = this.getDocumentOffset(iframe.ownerDocument);
66
+ return {
67
+ top: iframe.offsetTop + top,
68
+ left: iframe.offsetLeft + left,
69
+ };
70
+ };
71
+ }
72
+ if (currentWin &&
73
+ // Sometimes currentWin.window is null (not in specification). Such window object is not functional
74
+ currentWin === currentWin.window &&
75
+ !this.contextsSet.has(currentWin) // for each context callbacks called once per Tracker (TopObserver) instance
76
+ ) {
77
+ this.contextsSet.add(currentWin);
78
+ //@ts-ignore https://github.com/microsoft/TypeScript/issues/41684
79
+ this.contextCallbacks.forEach((cb) => cb(currentWin));
80
+ win = currentWin;
81
+ }
48
82
  });
49
- iframe.addEventListener("load", handle); // why app.attachEventListener not working?
83
+ iframe.addEventListener('load', handle); // why app.attachEventListener not working?
50
84
  handle();
51
85
  }
52
86
  handleShadowRoot(shRoot) {
@@ -58,25 +92,26 @@ export default class TopObserver extends Observer {
58
92
  // Protection from several subsequent calls?
59
93
  const observer = this;
60
94
  Element.prototype.attachShadow = function () {
95
+ // eslint-disable-next-line
61
96
  const shadow = attachShadowNativeFn.apply(this, arguments);
62
97
  observer.handleShadowRoot(shadow);
63
98
  return shadow;
64
99
  };
65
100
  // Can observe documentElement (<html>) here, because it is not supposed to be changing.
66
101
  // 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:
102
+ // In this case context.document have to be observed, but this will cause
103
+ // the change in the re-player behaviour caused by CreateDocument message:
69
104
  // the 0-node ("fRoot") will become #document rather than documentElement as it is now.
70
105
  // Alternatively - observe(#document) then bindNode(documentElement)
71
106
  this.observeRoot(window.document, () => {
72
- this.app.send(new CreateDocument());
107
+ this.app.send(CreateDocument());
73
108
  }, window.document.documentElement);
74
109
  }
75
110
  disconnect() {
76
111
  Element.prototype.attachShadow = attachShadowNativeFn;
77
- this.iframeObservers.forEach(o => o.disconnect());
112
+ this.iframeObservers.forEach((o) => o.disconnect());
78
113
  this.iframeObservers = [];
79
- this.shadowRootObservers.forEach(o => o.disconnect());
114
+ this.shadowRootObservers.forEach((o) => o.disconnect());
80
115
  this.shadowRootObservers = [];
81
116
  super.disconnect();
82
117
  }
@@ -1,4 +1,4 @@
1
- import type App from "./index.js";
1
+ import type App from './index.js';
2
2
  export interface Options {
3
3
  obscureTextEmails: boolean;
4
4
  obscureTextNumbers: boolean;
@@ -1,5 +1,5 @@
1
- import { stars, hasOpenreplayAttribute } from "../utils.js";
2
- import { isElementNode } from "./guards.js";
1
+ import { stars, hasOpenreplayAttribute } from '../utils.js';
2
+ import { isElementNode } from './guards.js';
3
3
  export default class Sanitizer {
4
4
  constructor(app, options) {
5
5
  this.app = app;
@@ -12,20 +12,20 @@ export default class Sanitizer {
12
12
  }
13
13
  handleNode(id, parentID, node) {
14
14
  if (this.masked.has(parentID) ||
15
- (isElementNode(node) &&
16
- hasOpenreplayAttribute(node, 'masked'))) {
15
+ (isElementNode(node) && hasOpenreplayAttribute(node, 'masked'))) {
17
16
  this.masked.add(id);
18
17
  }
19
18
  if (this.maskedContainers.has(parentID) ||
20
- (isElementNode(node) &&
21
- hasOpenreplayAttribute(node, 'htmlmasked'))) {
19
+ (isElementNode(node) && hasOpenreplayAttribute(node, 'htmlmasked'))) {
22
20
  this.maskedContainers.add(id);
23
21
  }
24
22
  }
25
23
  sanitize(id, data) {
26
24
  if (this.masked.has(id)) {
27
25
  // TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
28
- return data.trim().replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█');
26
+ return data
27
+ .trim()
28
+ .replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█');
29
29
  }
30
30
  if (this.options.obscureTextNumbers) {
31
31
  data = data.replace(/\d/g, '0');
@@ -1,19 +1,37 @@
1
+ import type App from './index.js';
1
2
  interface SessionInfo {
2
- sessionID: string | null;
3
+ sessionID: string | undefined;
3
4
  metadata: Record<string, string>;
4
5
  userID: string | null;
6
+ timestamp: number;
7
+ projectID?: string;
5
8
  }
6
9
  declare type OnUpdateCallback = (i: Partial<SessionInfo>) => void;
10
+ export declare type Options = {
11
+ session_token_key: string;
12
+ session_pageno_key: string;
13
+ };
7
14
  export default class Session {
15
+ private readonly app;
16
+ private readonly options;
8
17
  private metadata;
9
18
  private userID;
10
19
  private sessionID;
11
- private callbacks;
20
+ private readonly callbacks;
21
+ private timestamp;
22
+ private projectID;
23
+ constructor(app: App, options: Options);
12
24
  attachUpdateCallback(cb: OnUpdateCallback): void;
13
25
  private handleUpdate;
14
26
  update(newInfo: Partial<SessionInfo>): void;
15
27
  setMetadata(key: string, value: string): void;
16
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;
17
35
  getInfo(): SessionInfo;
18
36
  reset(): void;
19
37
  }
@@ -1,9 +1,11 @@
1
1
  export default class Session {
2
- constructor() {
2
+ constructor(app, options) {
3
+ this.app = app;
4
+ this.options = options;
3
5
  this.metadata = {};
4
6
  this.userID = null;
5
- this.sessionID = null;
6
7
  this.callbacks = [];
8
+ this.timestamp = 0;
7
9
  }
8
10
  attachUpdateCallback(cb) {
9
11
  this.callbacks.push(cb);
@@ -15,18 +17,25 @@ export default class Session {
15
17
  if (newInfo.sessionID == null) {
16
18
  delete newInfo.sessionID;
17
19
  }
18
- this.callbacks.forEach(cb => cb(newInfo));
20
+ this.callbacks.forEach((cb) => cb(newInfo));
19
21
  }
20
22
  update(newInfo) {
21
- if (newInfo.userID !== undefined) { // TODO clear nullable/undefinable types
23
+ if (newInfo.userID !== undefined) {
24
+ // TODO clear nullable/undefinable types
22
25
  this.userID = newInfo.userID;
23
26
  }
24
27
  if (newInfo.metadata !== undefined) {
25
- Object.entries(newInfo.metadata).forEach(([k, v]) => this.metadata[k] = v);
28
+ Object.entries(newInfo.metadata).forEach(([k, v]) => (this.metadata[k] = v));
26
29
  }
27
30
  if (newInfo.sessionID !== undefined) {
28
31
  this.sessionID = newInfo.sessionID;
29
32
  }
33
+ if (newInfo.timestamp !== undefined) {
34
+ this.timestamp = newInfo.timestamp;
35
+ }
36
+ if (newInfo.projectID !== undefined) {
37
+ this.projectID = newInfo.projectID;
38
+ }
30
39
  this.handleUpdate(newInfo);
31
40
  }
32
41
  setMetadata(key, value) {
@@ -37,16 +46,66 @@ export default class Session {
37
46
  this.userID = userID;
38
47
  this.handleUpdate({ userID });
39
48
  }
49
+ getPageNumber() {
50
+ const pageNoStr = this.app.sessionStorage.getItem(this.options.session_pageno_key);
51
+ if (pageNoStr == null) {
52
+ return undefined;
53
+ }
54
+ return parseInt(pageNoStr);
55
+ }
56
+ incPageNo() {
57
+ let pageNo = this.getPageNumber();
58
+ if (pageNo === undefined) {
59
+ pageNo = 0;
60
+ }
61
+ else {
62
+ pageNo++;
63
+ }
64
+ this.app.sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString());
65
+ return pageNo;
66
+ }
67
+ getSessionToken() {
68
+ return this.app.sessionStorage.getItem(this.options.session_token_key) || undefined;
69
+ }
70
+ setSessionToken(token) {
71
+ this.app.sessionStorage.setItem(this.options.session_token_key, token);
72
+ }
73
+ applySessionHash(hash) {
74
+ const hashParts = decodeURI(hash).split('&');
75
+ let token = hash;
76
+ let pageNoStr = '100500'; // back-compat for sessionToken
77
+ if (hashParts.length == 2) {
78
+ ;
79
+ [token, pageNoStr] = hashParts;
80
+ }
81
+ if (!pageNoStr || !token) {
82
+ return;
83
+ }
84
+ this.app.sessionStorage.setItem(this.options.session_token_key, token);
85
+ this.app.sessionStorage.setItem(this.options.session_pageno_key, pageNoStr);
86
+ }
87
+ getSessionHash() {
88
+ const pageNo = this.getPageNumber();
89
+ const token = this.getSessionToken();
90
+ if (pageNo === undefined || token === undefined) {
91
+ return;
92
+ }
93
+ return encodeURI(String(pageNo) + '&' + token);
94
+ }
40
95
  getInfo() {
41
96
  return {
42
97
  sessionID: this.sessionID,
43
98
  metadata: this.metadata,
44
99
  userID: this.userID,
100
+ timestamp: this.timestamp,
101
+ projectID: this.projectID,
45
102
  };
46
103
  }
47
104
  reset() {
105
+ this.app.sessionStorage.removeItem(this.options.session_token_key);
48
106
  this.metadata = {};
49
107
  this.userID = null;
50
- this.sessionID = null;
108
+ this.sessionID = undefined;
109
+ this.timestamp = 0;
51
110
  }
52
111
  }
@@ -1,4 +1,4 @@
1
- import App from "./index.js";
1
+ import App from './index.js';
2
2
  declare type Callback = () => void;
3
3
  export default class Ticker {
4
4
  private readonly app;