@openreplay/tracker 5.0.2 → 6.0.1-beta.1

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 (53) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/cjs/app/index.d.ts +3 -0
  3. package/cjs/app/index.js +4 -3
  4. package/cjs/app/messages.gen.d.ts +6 -1
  5. package/cjs/app/messages.gen.js +56 -5
  6. package/cjs/app/nodes.d.ts +1 -0
  7. package/cjs/app/nodes.js +3 -0
  8. package/cjs/app/observer/observer.js +7 -1
  9. package/cjs/app/observer/top_observer.js +4 -0
  10. package/cjs/app/ticker.d.ts +6 -0
  11. package/cjs/app/ticker.js +6 -0
  12. package/cjs/common/messages.gen.d.ts +46 -5
  13. package/cjs/index.d.ts +2 -0
  14. package/cjs/index.js +4 -2
  15. package/cjs/modules/img.js +1 -1
  16. package/cjs/modules/input.d.ts +2 -2
  17. package/cjs/modules/input.js +61 -39
  18. package/cjs/modules/mouse.d.ts +23 -1
  19. package/cjs/modules/mouse.js +43 -9
  20. package/cjs/modules/network.d.ts +1 -1
  21. package/cjs/modules/network.js +81 -10
  22. package/cjs/modules/selection.d.ts +7 -0
  23. package/cjs/modules/selection.js +37 -0
  24. package/cjs/modules/timing.js +3 -1
  25. package/cjs/utils.d.ts +2 -0
  26. package/cjs/utils.js +20 -1
  27. package/lib/app/index.d.ts +3 -0
  28. package/lib/app/index.js +4 -3
  29. package/lib/app/messages.gen.d.ts +6 -1
  30. package/lib/app/messages.gen.js +48 -2
  31. package/lib/app/nodes.d.ts +1 -0
  32. package/lib/app/nodes.js +3 -0
  33. package/lib/app/observer/observer.js +8 -2
  34. package/lib/app/observer/top_observer.js +5 -1
  35. package/lib/app/ticker.d.ts +6 -0
  36. package/lib/app/ticker.js +6 -0
  37. package/lib/common/messages.gen.d.ts +46 -5
  38. package/lib/common/tsconfig.tsbuildinfo +1 -1
  39. package/lib/index.d.ts +2 -0
  40. package/lib/index.js +4 -2
  41. package/lib/modules/img.js +1 -1
  42. package/lib/modules/input.d.ts +2 -2
  43. package/lib/modules/input.js +63 -41
  44. package/lib/modules/mouse.d.ts +23 -1
  45. package/lib/modules/mouse.js +45 -11
  46. package/lib/modules/network.d.ts +1 -1
  47. package/lib/modules/network.js +81 -10
  48. package/lib/modules/selection.d.ts +7 -0
  49. package/lib/modules/selection.js +35 -0
  50. package/lib/modules/timing.js +3 -1
  51. package/lib/utils.d.ts +2 -0
  52. package/lib/utils.js +17 -0
  53. package/package.json +1 -1
@@ -1,15 +1,15 @@
1
1
  import { hasTag, isSVGElement, isDocument } from '../app/guards.js';
2
- import { normSpaces, hasOpenreplayAttribute, getLabelAttribute } from '../utils.js';
3
- import { MouseMove, MouseClick } from '../app/messages.gen.js';
2
+ import { normSpaces, hasOpenreplayAttribute, getLabelAttribute, now } from '../utils.js';
3
+ import { MouseMove, MouseClick, MouseThrashing } from '../app/messages.gen.js';
4
4
  import { getInputLabel } from './input.js';
5
5
  import { finder } from '@medv/finder';
6
- function _getSelector(target, document) {
6
+ function _getSelector(target, document, options) {
7
7
  const selector = finder(target, {
8
8
  root: document.body,
9
9
  seedMinLength: 3,
10
- optimizedMinLength: 2,
11
- threshold: 1000,
12
- maxNumberOfTries: 10000,
10
+ optimizedMinLength: (options === null || options === void 0 ? void 0 : options.minSelectorDepth) || 2,
11
+ threshold: (options === null || options === void 0 ? void 0 : options.nthThreshold) || 1000,
12
+ maxNumberOfTries: (options === null || options === void 0 ? void 0 : options.maxOptimiseTries) || 10000,
13
13
  });
14
14
  return selector;
15
15
  }
@@ -24,7 +24,7 @@ function isClickable(element) {
24
24
  element.onclick != null ||
25
25
  element.getAttribute('role') === 'button');
26
26
  //|| element.className.includes("btn")
27
- // MBTODO: intersept addEventListener
27
+ // MBTODO: intercept addEventListener
28
28
  }
29
29
  //TODO: fix (typescript is not sure about target variable after assignation of svg)
30
30
  function getTarget(target, document) {
@@ -64,7 +64,8 @@ function _getTarget(target, document) {
64
64
  }
65
65
  return target === document.documentElement ? null : target;
66
66
  }
67
- export default function (app) {
67
+ export default function (app, options) {
68
+ const { disableClickmaps = false } = options || {};
68
69
  function getTargetLabel(target) {
69
70
  const dl = getLabelAttribute(target);
70
71
  if (dl !== null) {
@@ -89,12 +90,39 @@ export default function (app) {
89
90
  let mouseTarget = null;
90
91
  let mouseTargetTime = 0;
91
92
  let selectorMap = {};
93
+ let velocity = 0;
94
+ let direction = 0;
95
+ let directionChangeCount = 0;
96
+ let distance = 0;
97
+ let checkIntervalId;
98
+ const shakeThreshold = 0.008;
99
+ const shakeCheckInterval = 225;
100
+ function checkMouseShaking() {
101
+ const nextVelocity = distance / shakeCheckInterval;
102
+ if (!velocity) {
103
+ velocity = nextVelocity;
104
+ return;
105
+ }
106
+ const acceleration = (nextVelocity - velocity) / shakeCheckInterval;
107
+ if (directionChangeCount > 4 && acceleration > shakeThreshold) {
108
+ app.send(MouseThrashing(now()));
109
+ }
110
+ distance = 0;
111
+ directionChangeCount = 0;
112
+ velocity = nextVelocity;
113
+ }
114
+ app.attachStartCallback(() => {
115
+ checkIntervalId = setInterval(() => checkMouseShaking(), shakeCheckInterval);
116
+ });
92
117
  app.attachStopCallback(() => {
93
118
  mousePositionX = -1;
94
119
  mousePositionY = -1;
95
120
  mousePositionChanged = false;
96
121
  mouseTarget = null;
97
122
  selectorMap = {};
123
+ if (checkIntervalId) {
124
+ clearInterval(checkIntervalId);
125
+ }
98
126
  });
99
127
  const sendMouseMove = () => {
100
128
  if (mousePositionChanged) {
@@ -103,8 +131,8 @@ export default function (app) {
103
131
  }
104
132
  };
105
133
  const patchDocument = (document, topframe = false) => {
106
- function getSelector(id, target) {
107
- return (selectorMap[id] = selectorMap[id] || _getSelector(target, document));
134
+ function getSelector(id, target, options) {
135
+ return (selectorMap[id] = selectorMap[id] || _getSelector(target, document, options));
108
136
  }
109
137
  const attachListener = topframe
110
138
  ? app.attachEventListener.bind(app) // attached/removed on start/stop
@@ -121,6 +149,12 @@ export default function (app) {
121
149
  mousePositionX = e.clientX + left;
122
150
  mousePositionY = e.clientY + top;
123
151
  mousePositionChanged = true;
152
+ const nextDirection = Math.sign(e.movementX);
153
+ distance += Math.abs(e.movementX) + Math.abs(e.movementY);
154
+ if (nextDirection !== direction) {
155
+ direction = nextDirection;
156
+ directionChangeCount++;
157
+ }
124
158
  }, false);
125
159
  attachListener(document, 'click', (e) => {
126
160
  const target = getTarget(e.target, document);
@@ -130,7 +164,7 @@ export default function (app) {
130
164
  const id = app.nodes.getID(target);
131
165
  if (id !== undefined) {
132
166
  sendMouseMove();
133
- app.send(MouseClick(id, mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0, getTargetLabel(target), isClickable(target) ? getSelector(id, target) : ''), true);
167
+ app.send(MouseClick(id, mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0, getTargetLabel(target), isClickable(target) && !disableClickmaps ? getSelector(id, target, options) : ''), true);
134
168
  }
135
169
  mouseTarget = null;
136
170
  });
@@ -24,5 +24,5 @@ export interface Options {
24
24
  capturePayload: boolean;
25
25
  sanitizer?: Sanitizer;
26
26
  }
27
- export default function (app: App, opts?: Partial<Options>): void;
27
+ export default function (app: App, opts?: Partial<Options>, customEnv?: Record<string, any>): void;
28
28
  export {};
@@ -1,5 +1,42 @@
1
1
  import { NetworkRequest } from '../app/messages.gen.js';
2
2
  import { getTimeOrigin } from '../utils.js';
3
+ // Request:
4
+ // declare const enum BodyType {
5
+ // Blob = "Blob",
6
+ // ArrayBuffer = "ArrayBuffer",
7
+ // TypedArray = "TypedArray",
8
+ // DataView = "DataView",
9
+ // FormData = "FormData",
10
+ // URLSearchParams = "URLSearchParams",
11
+ // Document = "Document", // XHR only
12
+ // ReadableStream = "ReadableStream", // Fetch only
13
+ // Literal = "literal",
14
+ // Unknown = "unk",
15
+ // }
16
+ // XHRResponse body: ArrayBuffer, a Blob, a Document, a JavaScript Object, or a string
17
+ // TODO: extract maximum of useful information from any type of Request/Responce bodies
18
+ // function objectifyBody(body: any): RequestBody {
19
+ // if (body instanceof Blob) {
20
+ // return {
21
+ // body: `<Blob type: ${body.type}>; size: ${body.size}`,
22
+ // bodyType: BodyType.Blob,
23
+ // }
24
+ // }
25
+ // return {
26
+ // body,
27
+ // bodyType: BodyType.Literal,
28
+ // }
29
+ // }
30
+ function checkCacheByPerformanceTimings(requestUrl) {
31
+ if (performance) {
32
+ const timings = performance.getEntriesByName(requestUrl)[0];
33
+ if (timings) {
34
+ // @ts-ignore - weird ts typings, please refer to https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming
35
+ return timings.transferSize === 0 || timings.responseStart - timings.requestStart < 10;
36
+ }
37
+ }
38
+ return false;
39
+ }
3
40
  function getXHRRequestDataObject(xhr) {
4
41
  // @ts-ignore this is 3x faster than using Map<XHR, XHRRequestData>
5
42
  if (!xhr.__or_req_data__) {
@@ -12,7 +49,7 @@ function getXHRRequestDataObject(xhr) {
12
49
  function strMethod(method) {
13
50
  return typeof method === 'string' ? method.toUpperCase() : 'GET';
14
51
  }
15
- export default function (app, opts = {}) {
52
+ export default function (app, opts = {}, customEnv) {
16
53
  const options = Object.assign({
17
54
  failuresOnly: false,
18
55
  ignoreHeaders: ['Cookie', 'Set-Cookie', 'Authorization'],
@@ -63,8 +100,10 @@ export default function (app, opts = {}) {
63
100
  return JSON.stringify(r);
64
101
  }
65
102
  /* ====== Fetch ====== */
66
- const origFetch = window.fetch.bind(window);
67
- window.fetch = (input, init = {}) => {
103
+ const origFetch = customEnv
104
+ ? customEnv.fetch.bind(customEnv)
105
+ : window.fetch.bind(window);
106
+ const trackFetch = (input, init = {}) => {
68
107
  if (!(typeof input === 'string' || input instanceof URL) || app.isServiceURL(String(input))) {
69
108
  return origFetch(input, init);
70
109
  }
@@ -138,10 +177,19 @@ export default function (app, opts = {}) {
138
177
  return response;
139
178
  });
140
179
  };
180
+ if (customEnv) {
181
+ customEnv.fetch = trackFetch;
182
+ }
183
+ else {
184
+ window.fetch = trackFetch;
185
+ }
141
186
  /* ====== <> ====== */
142
187
  /* ====== XHR ====== */
143
- const nativeOpen = XMLHttpRequest.prototype.open;
144
- XMLHttpRequest.prototype.open = function (initMethod, url) {
188
+ const nativeOpen = customEnv
189
+ ? customEnv.XMLHttpRequest.prototype.open
190
+ : XMLHttpRequest.prototype.open;
191
+ function trackXMLHttpReqOpen(initMethod, url) {
192
+ // @ts-ignore ??? this -> XMLHttpRequest
145
193
  const xhr = this;
146
194
  setSessionTokenHeader((name, value) => xhr.setRequestHeader(name, value));
147
195
  let startTime = 0;
@@ -180,21 +228,44 @@ export default function (app, opts = {}) {
180
228
  }));
181
229
  //TODO: handle error (though it has no Error API nor any useful information)
182
230
  //xhr.addEventListener('error', (e) => {})
231
+ // @ts-ignore ??? this -> XMLHttpRequest
183
232
  return nativeOpen.apply(this, arguments);
184
- };
233
+ }
234
+ if (customEnv) {
235
+ customEnv.XMLHttpRequest.prototype.open = trackXMLHttpReqOpen.bind(customEnv);
236
+ }
237
+ else {
238
+ XMLHttpRequest.prototype.open = trackXMLHttpReqOpen;
239
+ }
185
240
  const nativeSend = XMLHttpRequest.prototype.send;
186
- XMLHttpRequest.prototype.send = function (body) {
241
+ function trackXHRSend(body) {
242
+ // @ts-ignore ??? this -> XMLHttpRequest
187
243
  const rdo = getXHRRequestDataObject(this);
188
244
  rdo.body = body;
245
+ // @ts-ignore ??? this -> XMLHttpRequest
189
246
  return nativeSend.apply(this, arguments);
190
- };
247
+ }
248
+ if (customEnv) {
249
+ customEnv.XMLHttpRequest.prototype.send = trackXHRSend.bind(customEnv);
250
+ }
251
+ else {
252
+ XMLHttpRequest.prototype.send = trackXHRSend;
253
+ }
191
254
  const nativeSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
192
- XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
255
+ function trackSetReqHeader(name, value) {
193
256
  if (!isHIgnored(name)) {
257
+ // @ts-ignore ??? this -> XMLHttpRequest
194
258
  const rdo = getXHRRequestDataObject(this);
195
259
  rdo.headers[name] = value;
196
260
  }
261
+ // @ts-ignore ??? this -> XMLHttpRequest
197
262
  return nativeSetRequestHeader.apply(this, arguments);
198
- };
263
+ }
264
+ if (customEnv) {
265
+ customEnv.XMLHttpRequest.prototype.setRequestHeader = trackSetReqHeader.bind(customEnv);
266
+ }
267
+ else {
268
+ XMLHttpRequest.prototype.setRequestHeader = trackSetReqHeader;
269
+ }
199
270
  /* ====== <> ====== */
200
271
  }
@@ -0,0 +1,7 @@
1
+ import type App from '../app/index.js';
2
+ declare function selection(app: App): void;
3
+ export default selection;
4
+ /** TODO: research how to get all in-between nodes inside selection range
5
+ * including nodes between anchor and focus nodes and their children
6
+ * without recursively searching the dom tree
7
+ */
@@ -0,0 +1,35 @@
1
+ import { SelectionChange } from '../app/messages.gen.js';
2
+ function selection(app) {
3
+ app.attachEventListener(document, 'selectionchange', () => {
4
+ const selection = document.getSelection();
5
+ if (selection !== null && !selection.isCollapsed) {
6
+ const selectionStart = app.nodes.getID(selection.anchorNode);
7
+ const selectionEnd = app.nodes.getID(selection.focusNode);
8
+ const selectedText = selection.toString().replace(/\s+/g, ' ');
9
+ if (selectionStart && selectionEnd) {
10
+ app.send(SelectionChange(selectionStart, selectionEnd, selectedText));
11
+ }
12
+ }
13
+ else {
14
+ app.send(SelectionChange(-1, -1, ''));
15
+ }
16
+ });
17
+ }
18
+ export default selection;
19
+ /** TODO: research how to get all in-between nodes inside selection range
20
+ * including nodes between anchor and focus nodes and their children
21
+ * without recursively searching the dom tree
22
+ */
23
+ // if (selection.rangeCount) {
24
+ // const nodes = [];
25
+ // for (let i = 0; i < selection.rangeCount; i++) {
26
+ // const range = selection.getRangeAt(i);
27
+ // let node: Node | null = range.startContainer;
28
+ // while (node) {
29
+ // nodes.push(node);
30
+ // if (node === range.endContainer) break;
31
+ // node = node.nextSibling;
32
+ // }
33
+ // }
34
+ // // send selected nodes
35
+ // }
@@ -73,7 +73,9 @@ export default function (app, opts) {
73
73
  if (resources !== null) {
74
74
  resources[entry.name] = entry.startTime + entry.duration;
75
75
  }
76
- app.send(ResourceTiming(entry.startTime + getTimeOrigin(), entry.duration, entry.responseStart && entry.startTime ? entry.responseStart - entry.startTime : 0, entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0, entry.encodedBodySize || 0, entry.decodedBodySize || 0, entry.name, entry.initiatorType));
76
+ app.send(ResourceTiming(entry.startTime + getTimeOrigin(), entry.duration, entry.responseStart && entry.startTime ? entry.responseStart - entry.startTime : 0, entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0, entry.encodedBodySize || 0, entry.decodedBodySize || 0, entry.name, entry.initiatorType, entry.transferSize,
77
+ // @ts-ignore
78
+ (entry.responseStatus && entry.responseStatus === 304) || entry.transferSize === 0));
77
79
  }
78
80
  const observer = new PerformanceObserver((list) => list.getEntries().forEach(resourceTiming));
79
81
  let prevSessionID;
package/lib/utils.d.ts CHANGED
@@ -11,3 +11,5 @@ export declare const DOCS_HOST = "https://docs.openreplay.com";
11
11
  export declare function deprecationWarn(nameOfFeature: string, useInstead: string, docsPath?: string): void;
12
12
  export declare function getLabelAttribute(e: Element): string | null;
13
13
  export declare function hasOpenreplayAttribute(e: Element, attr: string): boolean;
14
+ export declare function isIframeCrossdomain(e: HTMLIFrameElement): boolean;
15
+ export declare function canAccessIframe(iframe: HTMLIFrameElement): boolean;
package/lib/utils.js CHANGED
@@ -59,3 +59,20 @@ export function hasOpenreplayAttribute(e, attr) {
59
59
  }
60
60
  return false;
61
61
  }
62
+ export function isIframeCrossdomain(e) {
63
+ var _a;
64
+ try {
65
+ return ((_a = e.contentWindow) === null || _a === void 0 ? void 0 : _a.location.href) !== window.location.href;
66
+ }
67
+ catch (e) {
68
+ return true;
69
+ }
70
+ }
71
+ export function canAccessIframe(iframe) {
72
+ try {
73
+ return Boolean(iframe.contentDocument);
74
+ }
75
+ catch (e) {
76
+ return false;
77
+ }
78
+ }
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": "5.0.2",
4
+ "version": "6.0.1-beta.1",
5
5
  "keywords": [
6
6
  "logging",
7
7
  "replay"