@openreplay/tracker 4.0.1 → 4.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 (60) hide show
  1. package/cjs/app/guards.d.ts +1 -0
  2. package/cjs/app/guards.js +6 -1
  3. package/cjs/app/index.d.ts +1 -1
  4. package/cjs/app/index.js +16 -10
  5. package/cjs/app/messages.gen.js +1 -0
  6. package/cjs/app/nodes.d.ts +1 -1
  7. package/cjs/app/nodes.js +3 -5
  8. package/cjs/app/observer/iframe_observer.js +1 -0
  9. package/cjs/app/observer/iframe_offsets.d.ts +8 -0
  10. package/cjs/app/observer/iframe_offsets.js +59 -0
  11. package/cjs/app/observer/observer.js +4 -4
  12. package/cjs/app/observer/top_observer.d.ts +2 -4
  13. package/cjs/app/observer/top_observer.js +11 -21
  14. package/cjs/app/sanitizer.d.ts +10 -4
  15. package/cjs/app/sanitizer.js +33 -15
  16. package/cjs/app/session.js +1 -1
  17. package/cjs/common/messages.gen.js +1 -0
  18. package/cjs/index.d.ts +1 -0
  19. package/cjs/index.js +7 -7
  20. package/cjs/modules/constructedStyleSheets.d.ts +4 -0
  21. package/cjs/modules/{adoptedStyleSheets.js → constructedStyleSheets.js} +21 -20
  22. package/cjs/modules/cssrules.js +65 -18
  23. package/cjs/modules/img.js +27 -19
  24. package/cjs/modules/input.js +2 -2
  25. package/cjs/modules/mouse.js +11 -7
  26. package/cjs/modules/scroll.js +32 -12
  27. package/cjs/utils.d.ts +5 -3
  28. package/cjs/utils.js +18 -13
  29. package/lib/app/guards.d.ts +1 -0
  30. package/lib/app/guards.js +4 -0
  31. package/lib/app/index.d.ts +1 -1
  32. package/lib/app/index.js +16 -10
  33. package/lib/app/messages.gen.js +1 -0
  34. package/lib/app/nodes.d.ts +1 -1
  35. package/lib/app/nodes.js +3 -5
  36. package/lib/app/observer/iframe_observer.js +1 -0
  37. package/lib/app/observer/iframe_offsets.d.ts +8 -0
  38. package/lib/app/observer/iframe_offsets.js +56 -0
  39. package/lib/app/observer/observer.js +4 -4
  40. package/lib/app/observer/top_observer.d.ts +2 -4
  41. package/lib/app/observer/top_observer.js +11 -21
  42. package/lib/app/sanitizer.d.ts +10 -4
  43. package/lib/app/sanitizer.js +32 -15
  44. package/lib/app/session.js +1 -1
  45. package/lib/common/messages.gen.js +1 -0
  46. package/lib/common/tsconfig.tsbuildinfo +1 -1
  47. package/lib/index.d.ts +1 -0
  48. package/lib/index.js +5 -6
  49. package/lib/modules/constructedStyleSheets.d.ts +4 -0
  50. package/lib/modules/{adoptedStyleSheets.js → constructedStyleSheets.js} +20 -21
  51. package/lib/modules/cssrules.js +67 -19
  52. package/lib/modules/img.js +28 -20
  53. package/lib/modules/input.js +3 -3
  54. package/lib/modules/mouse.js +11 -7
  55. package/lib/modules/scroll.js +33 -13
  56. package/lib/utils.d.ts +5 -3
  57. package/lib/utils.js +17 -11
  58. package/package.json +1 -1
  59. package/cjs/modules/adoptedStyleSheets.d.ts +0 -2
  60. package/lib/modules/adoptedStyleSheets.d.ts +0 -2
package/lib/index.js CHANGED
@@ -3,6 +3,7 @@ export { default as App } from './app/index.js';
3
3
  import { UserAnonymousID, RawCustomEvent, CustomIssue } from './app/messages.gen.js';
4
4
  import * as _Messages from './app/messages.gen.js';
5
5
  export const Messages = _Messages;
6
+ export { SanitizeLevel } from './app/sanitizer.js';
6
7
  import Connection from './modules/connection.js';
7
8
  import Console from './modules/console.js';
8
9
  import Exception, { getExceptionMessageFromEvent, getExceptionMessage, } from './modules/exception.js';
@@ -14,7 +15,7 @@ import Performance from './modules/performance.js';
14
15
  import Scroll from './modules/scroll.js';
15
16
  import Viewport from './modules/viewport.js';
16
17
  import CSSRules from './modules/cssrules.js';
17
- import AdoptedStyleSheets from './modules/adoptedStyleSheets.js';
18
+ import ConstructedStyleSheets from './modules/constructedStyleSheets.js';
18
19
  import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js';
19
20
  const DOCS_SETUP = '/installation/setup-or';
20
21
  function processOptions(obj) {
@@ -93,7 +94,7 @@ export default class API {
93
94
  if (app !== null) {
94
95
  Viewport(app);
95
96
  CSSRules(app);
96
- AdoptedStyleSheets(app);
97
+ ConstructedStyleSheets(app);
97
98
  Connection(app);
98
99
  Console(app, options);
99
100
  Exception(app, options);
@@ -127,7 +128,7 @@ export default class API {
127
128
  // no-cors issue only with text/plain or not-set Content-Type
128
129
  // req.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
129
130
  req.send(JSON.stringify({
130
- trackerVersion: '4.0.1',
131
+ trackerVersion: '4.0.2',
131
132
  projectKey: options.projectKey,
132
133
  doNotTrack,
133
134
  // TODO: add precise reason (an exact API missing)
@@ -159,9 +160,7 @@ export default class API {
159
160
  return;
160
161
  }
161
162
  this.app.stop();
162
- const sessionHash = this.app.session.getSessionHash();
163
- this.app.session.reset();
164
- return sessionHash;
163
+ return this.app.session.getSessionHash();
165
164
  }
166
165
  getSessionToken() {
167
166
  if (this.app === null) {
@@ -0,0 +1,4 @@
1
+ import type App from '../app/index.js';
2
+ export declare function nextID(): number;
3
+ export declare const styleSheetIDMap: Map<CSSStyleSheet, number>;
4
+ export default function (app: App | null): void;
@@ -1,10 +1,16 @@
1
- import { TechnicalInfo, AdoptedSSReplaceURLBased, AdoptedSSInsertRuleURLBased, AdoptedSSDeleteRule, AdoptedSSAddOwner, AdoptedSSRemoveOwner, } from '../app/messages.gen.js';
1
+ import { TechnicalInfo, AdoptedSSReplaceURLBased, AdoptedSSInsertRuleURLBased, AdoptedSSAddOwner, AdoptedSSRemoveOwner, } from '../app/messages.gen.js';
2
2
  import { isRootNode } from '../app/guards.js';
3
3
  function hasAdoptedSS(node) {
4
4
  return (isRootNode(node) &&
5
5
  // @ts-ignore
6
6
  !!node.adoptedStyleSheets);
7
7
  }
8
+ // TODO: incapsulate to be init-ed on-start and join with cssrules.ts under one folder
9
+ let _id = 0xf;
10
+ export function nextID() {
11
+ return _id++;
12
+ }
13
+ export const styleSheetIDMap = new Map();
8
14
  export default function (app) {
9
15
  if (app === null) {
10
16
  return;
@@ -16,10 +22,9 @@ export default function (app) {
16
22
  });
17
23
  return;
18
24
  }
19
- let nextID = 0xf;
20
25
  const styleSheetIDMap = new Map();
21
26
  const adoptedStyleSheetsOwnings = new Map();
22
- const updateAdoptedStyleSheets = (root) => {
27
+ const sendAdoptedStyleSheetsUpdate = (root) => {
23
28
  let nodeID = app.nodes.getID(root);
24
29
  if (root === document) {
25
30
  nodeID = 0; // main document doesn't have nodeID. ID count starts from the documentElement
@@ -37,7 +42,7 @@ export default function (app) {
37
42
  let sheetID = styleSheetIDMap.get(s);
38
43
  const init = !sheetID;
39
44
  if (!sheetID) {
40
- sheetID = ++nextID;
45
+ sheetID = nextID();
41
46
  }
42
47
  nowOwning.push(sheetID);
43
48
  if (!pastOwning.includes(sheetID)) {
@@ -63,12 +68,20 @@ export default function (app) {
63
68
  Object.defineProperty(prototype, 'adoptedStyleSheets', Object.assign(Object.assign({}, nativeAdoptedStyleSheetsDescriptor), { set: function (value) {
64
69
  // @ts-ignore
65
70
  const retVal = nativeAdoptedStyleSheetsDescriptor.set.call(this, value);
66
- updateAdoptedStyleSheets(this);
71
+ sendAdoptedStyleSheetsUpdate(this);
67
72
  return retVal;
68
73
  } }));
69
74
  }
70
75
  }
71
76
  const patchContext = (context) => {
77
+ // @ts-ignore
78
+ if (context.__openreplay_adpss_patched__) {
79
+ return;
80
+ }
81
+ else {
82
+ // @ts-ignore
83
+ context.__openreplay_adpss_patched__ = true;
84
+ }
72
85
  patchAdoptedStyleSheets(context.Document.prototype);
73
86
  patchAdoptedStyleSheets(context.ShadowRoot.prototype);
74
87
  //@ts-ignore TODO: configure ts (use necessary lib)
@@ -91,20 +104,6 @@ export default function (app) {
91
104
  }
92
105
  return replaceSync.call(this, text);
93
106
  };
94
- context.CSSStyleSheet.prototype.insertRule = function (rule, index = 0) {
95
- const sheetID = styleSheetIDMap.get(this);
96
- if (sheetID) {
97
- app.send(AdoptedSSInsertRuleURLBased(sheetID, rule, index, app.getBaseHref()));
98
- }
99
- return insertRule.call(this, rule, index);
100
- };
101
- context.CSSStyleSheet.prototype.deleteRule = function (index) {
102
- const sheetID = styleSheetIDMap.get(this);
103
- if (sheetID) {
104
- app.send(AdoptedSSDeleteRule(sheetID, index));
105
- }
106
- return deleteRule.call(this, index);
107
- };
108
107
  };
109
108
  patchContext(window);
110
109
  app.observer.attachContextCallback(patchContext);
@@ -114,11 +113,11 @@ export default function (app) {
114
113
  });
115
114
  // So far main Document is not triggered with nodeCallbacks
116
115
  app.attachStartCallback(() => {
117
- updateAdoptedStyleSheets(document);
116
+ sendAdoptedStyleSheetsUpdate(document);
118
117
  });
119
118
  app.nodes.attachNodeCallback((node) => {
120
119
  if (hasAdoptedSS(node)) {
121
- updateAdoptedStyleSheets(node);
120
+ sendAdoptedStyleSheetsUpdate(node);
122
121
  }
123
122
  });
124
123
  }
@@ -1,5 +1,7 @@
1
- import { CSSInsertRuleURLBased, CSSDeleteRule, TechnicalInfo } from '../app/messages.gen.js';
1
+ import { AdoptedSSInsertRuleURLBased, // TODO: rename to common StyleSheet names
2
+ AdoptedSSDeleteRule, AdoptedSSAddOwner, TechnicalInfo, } from '../app/messages.gen.js';
2
3
  import { hasTag } from '../app/guards.js';
4
+ import { nextID, styleSheetIDMap } from './constructedStyleSheets.js';
3
5
  export default function (app) {
4
6
  if (app === null) {
5
7
  return;
@@ -8,42 +10,88 @@ export default function (app) {
8
10
  app.send(TechnicalInfo('no_stylesheet_prototype_in_window', ''));
9
11
  return;
10
12
  }
11
- const processOperation = app.safe((stylesheet, index, rule) => {
12
- const sendMessage = typeof rule === 'string'
13
- ? (nodeID) => app.send(CSSInsertRuleURLBased(nodeID, rule, index, app.getBaseHref()))
14
- : (nodeID) => app.send(CSSDeleteRule(nodeID, index));
15
- // TODO: Extend messages to maintain nested rules (CSSGroupingRule prototype, as well as CSSKeyframesRule)
16
- if (stylesheet.ownerNode == null) {
17
- throw new Error('Owner Node not found');
18
- }
19
- const nodeID = app.nodes.getID(stylesheet.ownerNode);
20
- if (nodeID !== undefined) {
21
- sendMessage(nodeID);
22
- } // else error?
13
+ const sendInserDeleteRule = app.safe((sheet, index, rule) => {
14
+ const sheetID = styleSheetIDMap.get(sheet);
15
+ if (!sheetID) {
16
+ // OK-case. Sheet haven't been registered yet. Rules will be sent on registration.
17
+ return;
18
+ }
19
+ if (typeof rule === 'string') {
20
+ app.send(AdoptedSSInsertRuleURLBased(sheetID, rule, index, app.getBaseHref()));
21
+ }
22
+ else {
23
+ app.send(AdoptedSSDeleteRule(sheetID, index));
24
+ }
25
+ });
26
+ // TODO: proper rule insertion/removal (how?)
27
+ const sendReplaceGroupingRule = app.safe((rule) => {
28
+ let topmostRule = rule;
29
+ while (topmostRule.parentRule) {
30
+ topmostRule = topmostRule.parentRule;
31
+ }
32
+ const sheet = topmostRule.parentStyleSheet;
33
+ if (!sheet) {
34
+ app.debug.warn('No parent StyleSheet found for', topmostRule, rule);
35
+ return;
36
+ }
37
+ const sheetID = styleSheetIDMap.get(sheet);
38
+ if (!sheetID) {
39
+ app.debug.warn('No sheedID found for', sheet, styleSheetIDMap);
40
+ return;
41
+ }
42
+ const cssText = topmostRule.cssText;
43
+ const ruleList = sheet.cssRules;
44
+ const idx = Array.from(ruleList).indexOf(topmostRule);
45
+ if (idx >= 0) {
46
+ app.send(AdoptedSSInsertRuleURLBased(sheetID, cssText, idx, app.getBaseHref()));
47
+ app.send(AdoptedSSDeleteRule(sheetID, idx + 1)); // Remove previous clone
48
+ }
49
+ else {
50
+ app.debug.warn('Rule index not found in', sheet, topmostRule);
51
+ }
23
52
  });
24
53
  const patchContext = (context) => {
25
54
  const { insertRule, deleteRule } = context.CSSStyleSheet.prototype;
55
+ const { insertRule: groupInsertRule, deleteRule: groupDeleteRule } = context.CSSGroupingRule.prototype;
26
56
  context.CSSStyleSheet.prototype.insertRule = function (rule, index = 0) {
27
- processOperation(this, index, rule);
57
+ sendInserDeleteRule(this, index, rule);
28
58
  return insertRule.call(this, rule, index);
29
59
  };
30
60
  context.CSSStyleSheet.prototype.deleteRule = function (index) {
31
- processOperation(this, index);
61
+ sendInserDeleteRule(this, index);
32
62
  return deleteRule.call(this, index);
33
63
  };
64
+ context.CSSGroupingRule.prototype.insertRule = function (rule, index = 0) {
65
+ const result = groupInsertRule.call(this, rule, index);
66
+ sendReplaceGroupingRule(this);
67
+ return result;
68
+ };
69
+ context.CSSGroupingRule.prototype.deleteRule = function (index = 0) {
70
+ const result = groupDeleteRule.call(this, index);
71
+ sendReplaceGroupingRule(this);
72
+ return result;
73
+ };
34
74
  };
35
75
  patchContext(window);
36
76
  app.observer.attachContextCallback(patchContext);
37
77
  app.nodes.attachNodeCallback((node) => {
38
- if (!hasTag(node, 'STYLE') || !node.sheet) {
78
+ if (!(hasTag(node, 'STYLE') || hasTag(node, 'style')) || !node.sheet) {
39
79
  return;
40
80
  }
41
81
  if (node.textContent !== null && node.textContent.trim().length > 0) {
42
- return; // Only fully virtual sheets maintained so far
82
+ return; // Non-virtual styles captured by the observer as a text
83
+ }
84
+ const nodeID = app.nodes.getID(node);
85
+ if (!nodeID) {
86
+ return;
43
87
  }
44
- const rules = node.sheet.cssRules;
88
+ const sheet = node.sheet;
89
+ const sheetID = nextID();
90
+ styleSheetIDMap.set(sheet, sheetID);
91
+ app.send(AdoptedSSAddOwner(sheetID, nodeID));
92
+ const rules = sheet.cssRules;
45
93
  for (let i = 0; i < rules.length; i++) {
46
- processOperation(node.sheet, i, rules[i].cssText);
94
+ sendInserDeleteRule(sheet, i, rules[i].cssText);
47
95
  }
48
96
  });
49
97
  }
@@ -1,4 +1,4 @@
1
- import { timestamp, isURL } from '../utils.js';
1
+ import { timestamp, isURL, IS_FIREFOX, MAX_STR_LEN } from '../utils.js';
2
2
  import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from '../app/messages.gen.js';
3
3
  import { hasTag } from '../app/guards.js';
4
4
  function resolveURL(url, location = document.location) {
@@ -8,7 +8,7 @@ function resolveURL(url, location = document.location) {
8
8
  }
9
9
  else if (url.startsWith('http://') ||
10
10
  url.startsWith('https://') ||
11
- url.startsWith('data:') // any other possible value here?
11
+ url.startsWith('data:') // any other possible value here? https://bugzilla.mozilla.org/show_bug.cgi?id=1758035
12
12
  ) {
13
13
  return url;
14
14
  }
@@ -16,6 +16,10 @@ function resolveURL(url, location = document.location) {
16
16
  return location.origin + location.pathname + url;
17
17
  }
18
18
  }
19
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1607081
20
+ function isSVGInFireFox(url) {
21
+ return IS_FIREFOX && (url.startsWith('data:image/svg+xml') || url.match(/.svg$|/i));
22
+ }
19
23
  const PLACEHOLDER_SRC = 'https://static.openreplay.com/tracker/placeholder.jpeg';
20
24
  export default function (app) {
21
25
  function sendPlaceholder(id, node) {
@@ -40,30 +44,34 @@ export default function (app) {
40
44
  app.send(SetNodeAttribute(id, 'srcset', resolvedSrcset));
41
45
  };
42
46
  const sendSrc = function (id, img) {
43
- const src = img.src;
44
- app.send(SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref()));
47
+ if (img.src.length > MAX_STR_LEN) {
48
+ sendPlaceholder(id, img);
49
+ }
50
+ app.send(SetNodeAttributeURLBased(id, 'src', img.src, app.getBaseHref()));
45
51
  };
46
- const sendImgAttrs = app.safe(function () {
47
- const id = app.nodes.getID(this);
52
+ const sendImgError = app.safe(function (img) {
53
+ const resolvedSrc = resolveURL(img.src || ''); // Src type is null sometimes. - is it true?
54
+ if (isURL(resolvedSrc)) {
55
+ app.send(ResourceTiming(timestamp(), 0, 0, 0, 0, 0, resolvedSrc, 'img'));
56
+ }
57
+ });
58
+ const sendImgAttrs = app.safe(function (img) {
59
+ const id = app.nodes.getID(img);
48
60
  if (id === undefined) {
49
61
  return;
50
62
  }
51
- const { src, complete, naturalWidth, naturalHeight, srcset } = this;
52
- if (!complete) {
63
+ if (!img.complete) {
53
64
  return;
54
65
  }
55
- const resolvedSrc = resolveURL(src || ''); // Src type is null sometimes. - is it true?
56
- if (naturalWidth === 0 && naturalHeight === 0) {
57
- if (isURL(resolvedSrc)) {
58
- app.send(ResourceTiming(timestamp(), 0, 0, 0, 0, 0, resolvedSrc, 'img'));
59
- }
66
+ if (img.naturalHeight === 0 && img.naturalWidth === 0 && !isSVGInFireFox(img.src)) {
67
+ sendImgError(img);
60
68
  }
61
- else if (resolvedSrc.length >= 1e5 || app.sanitizer.isMasked(id)) {
62
- sendPlaceholder(id, this);
69
+ else if (app.sanitizer.isHidden(id) || app.sanitizer.isObscured(id)) {
70
+ sendPlaceholder(id, img);
63
71
  }
64
72
  else {
65
- sendSrc(id, this);
66
- sendSrcset(id, this);
73
+ sendSrc(id, img);
74
+ sendSrcset(id, img);
67
75
  }
68
76
  });
69
77
  const observer = new MutationObserver((mutations) => {
@@ -90,9 +98,9 @@ export default function (app) {
90
98
  if (!hasTag(node, 'IMG')) {
91
99
  return;
92
100
  }
93
- app.nodes.attachElementListener('error', node, sendImgAttrs.bind(node));
94
- app.nodes.attachElementListener('load', node, sendImgAttrs.bind(node));
95
- sendImgAttrs.call(node);
101
+ app.nodes.attachNodeListener(node, 'error', () => sendImgError(node));
102
+ app.nodes.attachNodeListener(node, 'load', () => sendImgAttrs(node));
103
+ sendImgAttrs(node);
96
104
  observer.observe(node, { attributes: true, attributeFilter: ['src', 'srcset'] });
97
105
  });
98
106
  }
@@ -1,4 +1,4 @@
1
- import { normSpaces, IN_BROWSER, getLabelAttribute, hasOpenreplayAttribute } from '../utils.js';
1
+ import { normSpaces, IN_BROWSER, getLabelAttribute } from '../utils.js';
2
2
  import { hasTag } from '../app/guards.js';
3
3
  import { SetInputTarget, SetInputValue, SetInputChecked } from '../app/messages.gen.js';
4
4
  const INPUT_TYPES = ['text', 'password', 'email', 'search', 'number', 'range', 'date'];
@@ -76,10 +76,10 @@ export default function (app, opts) {
76
76
  function sendInputValue(id, node) {
77
77
  let value = node.value;
78
78
  let inputMode = options.defaultInputMode;
79
- if (node.type === 'password' || hasOpenreplayAttribute(node, 'hidden')) {
79
+ if (node.type === 'password' || app.sanitizer.isHidden(id)) {
80
80
  inputMode = 2 /* Hidden */;
81
81
  }
82
- else if (hasOpenreplayAttribute(node, 'obscured') ||
82
+ else if (app.sanitizer.isObscured(id) ||
83
83
  (inputMode === 0 /* Plain */ &&
84
84
  ((options.obscureInputNumbers && node.type !== 'date' && /\d\d\d\d/.test(value)) ||
85
85
  (options.obscureInputDates && node.type === 'date') ||
@@ -96,11 +96,13 @@ export default function (app) {
96
96
  let mousePositionChanged = false;
97
97
  let mouseTarget = null;
98
98
  let mouseTargetTime = 0;
99
+ let selectorMap = {};
99
100
  app.attachStopCallback(() => {
100
101
  mousePositionX = -1;
101
102
  mousePositionY = -1;
102
103
  mousePositionChanged = false;
103
104
  mouseTarget = null;
105
+ selectorMap = {};
104
106
  });
105
107
  const sendMouseMove = () => {
106
108
  if (mousePositionChanged) {
@@ -108,25 +110,27 @@ export default function (app) {
108
110
  mousePositionChanged = false;
109
111
  }
110
112
  };
111
- const patchDocument = (document) => {
112
- const selectorMap = {};
113
+ const patchDocument = (document, topframe = false) => {
113
114
  function getSelector(id, target) {
114
115
  return (selectorMap[id] = selectorMap[id] || _getSelector(target, document));
115
116
  }
116
- app.attachEventListener(document.documentElement, 'mouseover', (e) => {
117
+ const attachListener = topframe
118
+ ? app.attachEventListener.bind(app) // attached/removed on start/stop
119
+ : app.nodes.attachNodeListener.bind(app.nodes); // attached/removed on node register/unregister
120
+ attachListener(document.documentElement, 'mouseover', (e) => {
117
121
  const target = getTarget(e.target, document);
118
122
  if (target !== mouseTarget) {
119
123
  mouseTarget = target;
120
124
  mouseTargetTime = performance.now();
121
125
  }
122
126
  });
123
- app.attachEventListener(document, 'mousemove', (e) => {
124
- const { top, left } = app.observer.getDocumentOffset(document);
127
+ attachListener(document, 'mousemove', (e) => {
128
+ const [left, top] = app.observer.getDocumentOffset(document); // MBTODO?: document-id related message
125
129
  mousePositionX = e.clientX + left;
126
130
  mousePositionY = e.clientY + top;
127
131
  mousePositionChanged = true;
128
132
  }, false);
129
- app.attachEventListener(document, 'click', (e) => {
133
+ attachListener(document, 'click', (e) => {
130
134
  const target = getTarget(e.target, document);
131
135
  if ((!e.clientX && !e.clientY) || target === null) {
132
136
  return;
@@ -144,6 +148,6 @@ export default function (app) {
144
148
  patchDocument(node);
145
149
  }
146
150
  });
147
- patchDocument(document);
151
+ patchDocument(document, true);
148
152
  app.ticker.attach(sendMouseMove, 10);
149
153
  }
@@ -1,20 +1,33 @@
1
1
  import { SetViewportScroll, SetNodeScroll } from '../app/messages.gen.js';
2
- import { isElementNode, isRootNode } from '../app/guards.js';
2
+ import { isNode, isElementNode, isRootNode, isDocument } from '../app/guards.js';
3
+ function getDocumentScroll(doc) {
4
+ const win = doc.defaultView;
5
+ return [
6
+ (win && win.pageXOffset) ||
7
+ (doc.documentElement && doc.documentElement.scrollLeft) ||
8
+ (doc.body && doc.body.scrollLeft) ||
9
+ 0,
10
+ (win && win.pageYOffset) ||
11
+ (doc.documentElement && doc.documentElement.scrollTop) ||
12
+ (doc.body && doc.body.scrollTop) ||
13
+ 0,
14
+ ];
15
+ }
3
16
  export default function (app) {
4
17
  let documentScroll = false;
5
18
  const nodeScroll = new Map();
6
19
  function setNodeScroll(target) {
7
- if (target instanceof Element) {
20
+ if (!isNode(target)) {
21
+ return;
22
+ }
23
+ if (isElementNode(target)) {
8
24
  nodeScroll.set(target, [target.scrollLeft, target.scrollTop]);
9
25
  }
26
+ if (isDocument(target)) {
27
+ nodeScroll.set(target, getDocumentScroll(target));
28
+ }
10
29
  }
11
- const sendSetViewportScroll = app.safe(() => app.send(SetViewportScroll(window.pageXOffset ||
12
- (document.documentElement && document.documentElement.scrollLeft) ||
13
- (document.body && document.body.scrollLeft) ||
14
- 0, window.pageYOffset ||
15
- (document.documentElement && document.documentElement.scrollTop) ||
16
- (document.body && document.body.scrollTop) ||
17
- 0)));
30
+ const sendSetViewportScroll = app.safe(() => app.send(SetViewportScroll(...getDocumentScroll(document))));
18
31
  const sendSetNodeScroll = app.safe((s, node) => {
19
32
  const id = app.nodes.getID(node);
20
33
  if (id !== undefined) {
@@ -27,12 +40,19 @@ export default function (app) {
27
40
  nodeScroll.clear();
28
41
  });
29
42
  app.nodes.attachNodeCallback((node, isStart) => {
30
- if (isStart && isElementNode(node) && node.scrollLeft + node.scrollTop > 0) {
31
- nodeScroll.set(node, [node.scrollLeft, node.scrollTop]);
43
+ // MBTODO: iterate over all the nodes on start instead of using isStart hack
44
+ if (isStart) {
45
+ if (isElementNode(node) && node.scrollLeft + node.scrollTop > 0) {
46
+ nodeScroll.set(node, [node.scrollLeft, node.scrollTop]);
47
+ }
48
+ else if (isDocument(node)) {
49
+ // DRY somehow?
50
+ nodeScroll.set(node, getDocumentScroll(node));
51
+ }
32
52
  }
33
- else if (isRootNode(node)) {
53
+ if (isRootNode(node)) {
34
54
  // scroll is not-composed event (https://javascript.info/shadow-dom-events)
35
- app.attachEventListener(node, 'scroll', (e) => {
55
+ app.nodes.attachNodeListener(node, 'scroll', (e) => {
36
56
  setNodeScroll(e.target);
37
57
  });
38
58
  }
package/lib/utils.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- export declare function timestamp(): number;
1
+ export declare const IN_BROWSER: boolean;
2
+ export declare const IS_FIREFOX: false | RegExpMatchArray | null;
3
+ export declare const MAX_STR_LEN = 100000;
4
+ export declare const timestamp: () => number;
2
5
  export declare const stars: (str: string) => string;
3
6
  export declare function normSpaces(str: string): string;
4
7
  export declare function isURL(s: string): boolean;
5
- export declare const IN_BROWSER: boolean;
6
8
  export declare const DOCS_HOST = "https://docs.openreplay.com";
7
9
  export declare function deprecationWarn(nameOfFeature: string, useInstead: string, docsPath?: string): void;
8
10
  export declare function getLabelAttribute(e: Element): string | null;
9
- export declare function hasOpenreplayAttribute(e: Element, name: string): boolean;
11
+ export declare function hasOpenreplayAttribute(e: Element, attr: string): boolean;
package/lib/utils.js CHANGED
@@ -1,6 +1,12 @@
1
- export function timestamp() {
2
- return Math.round(performance.now()) + performance.timing.navigationStart;
3
- }
1
+ const DEPRECATED_ATTRS = { htmlmasked: 'hidden', masked: 'obscured' };
2
+ export const IN_BROWSER = !(typeof window === 'undefined');
3
+ export const IS_FIREFOX = IN_BROWSER && navigator.userAgent.match(/firefox|fxios/i);
4
+ export const MAX_STR_LEN = 1e5;
5
+ const navigationStart = (IN_BROWSER && performance.timing.navigationStart) || performance.timeOrigin;
6
+ // performance.now() is buggy in some browsers
7
+ export const timestamp = IN_BROWSER && performance.now() && navigationStart
8
+ ? () => Math.round(performance.now() + navigationStart)
9
+ : () => Date.now();
4
10
  export const stars = 'repeat' in String.prototype
5
11
  ? (str) => '*'.repeat(str.length)
6
12
  : (str) => str.replace(/./g, '*');
@@ -11,7 +17,6 @@ export function normSpaces(str) {
11
17
  export function isURL(s) {
12
18
  return s.startsWith('https://') || s.startsWith('http://');
13
19
  }
14
- export const IN_BROWSER = !(typeof window === 'undefined');
15
20
  // TODO: JOIN IT WITH LOGGER somehow (use logging decorators?); Don't forget about index.js loggin when there is no logger instance.
16
21
  export const DOCS_HOST = 'https://docs.openreplay.com';
17
22
  const warnedFeatures = {};
@@ -33,14 +38,15 @@ export function getLabelAttribute(e) {
33
38
  }
34
39
  return value;
35
40
  }
36
- export function hasOpenreplayAttribute(e, name) {
37
- const newName = `data-openreplay-${name}`;
41
+ export function hasOpenreplayAttribute(e, attr) {
42
+ const newName = `data-openreplay-${attr}`;
38
43
  if (e.hasAttribute(newName)) {
39
- return true;
40
- }
41
- const oldName = `data-asayer-${name}`;
42
- if (e.hasAttribute(oldName)) {
43
- deprecationWarn(`"${oldName}" attribute`, `"${newName}" attribute`, '/installation/sanitize-data');
44
+ // @ts-ignore
45
+ if (DEPRECATED_ATTRS[attr]) {
46
+ deprecationWarn(`"${newName}" attribute`,
47
+ // @ts-ignore
48
+ `"${DEPRECATED_ATTRS[attr]}" attribute`, '/installation/sanitize-data');
49
+ }
44
50
  return true;
45
51
  }
46
52
  return false;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openreplay/tracker",
3
3
  "description": "The OpenReplay tracker main package",
4
- "version": "4.0.1",
4
+ "version": "4.0.2",
5
5
  "keywords": [
6
6
  "logging",
7
7
  "replay"
@@ -1,2 +0,0 @@
1
- import type App from '../app/index.js';
2
- export default function (app: App | null): void;
@@ -1,2 +0,0 @@
1
- import type App from '../app/index.js';
2
- export default function (app: App | null): void;