@openreplay/tracker 5.0.2-beta.1 → 6.0.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 (49) hide show
  1. package/CHANGELOG.md +13 -0
  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 +2 -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 +44 -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/lib/app/index.d.ts +3 -0
  26. package/lib/app/index.js +4 -3
  27. package/lib/app/messages.gen.d.ts +6 -1
  28. package/lib/app/messages.gen.js +48 -2
  29. package/lib/app/nodes.d.ts +1 -0
  30. package/lib/app/nodes.js +3 -0
  31. package/lib/app/observer/observer.js +8 -2
  32. package/lib/app/observer/top_observer.js +2 -0
  33. package/lib/app/ticker.d.ts +6 -0
  34. package/lib/app/ticker.js +6 -0
  35. package/lib/common/messages.gen.d.ts +46 -5
  36. package/lib/common/tsconfig.tsbuildinfo +1 -1
  37. package/lib/index.d.ts +2 -0
  38. package/lib/index.js +4 -2
  39. package/lib/modules/img.js +1 -1
  40. package/lib/modules/input.d.ts +2 -2
  41. package/lib/modules/input.js +63 -41
  42. package/lib/modules/mouse.d.ts +23 -1
  43. package/lib/modules/mouse.js +46 -11
  44. package/lib/modules/network.d.ts +1 -1
  45. package/lib/modules/network.js +81 -10
  46. package/lib/modules/selection.d.ts +7 -0
  47. package/lib/modules/selection.js +35 -0
  48. package/lib/modules/timing.js +3 -1
  49. 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,40 @@ 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 > 3 && acceleration > shakeThreshold) {
108
+ console.log('Mouse shake detected!');
109
+ app.send(MouseThrashing(now()));
110
+ }
111
+ distance = 0;
112
+ directionChangeCount = 0;
113
+ velocity = nextVelocity;
114
+ }
115
+ app.attachStartCallback(() => {
116
+ checkIntervalId = setInterval(() => checkMouseShaking(), shakeCheckInterval);
117
+ });
92
118
  app.attachStopCallback(() => {
93
119
  mousePositionX = -1;
94
120
  mousePositionY = -1;
95
121
  mousePositionChanged = false;
96
122
  mouseTarget = null;
97
123
  selectorMap = {};
124
+ if (checkIntervalId) {
125
+ clearInterval(checkIntervalId);
126
+ }
98
127
  });
99
128
  const sendMouseMove = () => {
100
129
  if (mousePositionChanged) {
@@ -103,8 +132,8 @@ export default function (app) {
103
132
  }
104
133
  };
105
134
  const patchDocument = (document, topframe = false) => {
106
- function getSelector(id, target) {
107
- return (selectorMap[id] = selectorMap[id] || _getSelector(target, document));
135
+ function getSelector(id, target, options) {
136
+ return (selectorMap[id] = selectorMap[id] || _getSelector(target, document, options));
108
137
  }
109
138
  const attachListener = topframe
110
139
  ? app.attachEventListener.bind(app) // attached/removed on start/stop
@@ -121,6 +150,12 @@ export default function (app) {
121
150
  mousePositionX = e.clientX + left;
122
151
  mousePositionY = e.clientY + top;
123
152
  mousePositionChanged = true;
153
+ const nextDirection = Math.sign(e.movementX);
154
+ distance += Math.abs(e.movementX) + Math.abs(e.movementY);
155
+ if (nextDirection !== direction) {
156
+ direction = nextDirection;
157
+ directionChangeCount++;
158
+ }
124
159
  }, false);
125
160
  attachListener(document, 'click', (e) => {
126
161
  const target = getTarget(e.target, document);
@@ -130,7 +165,7 @@ export default function (app) {
130
165
  const id = app.nodes.getID(target);
131
166
  if (id !== undefined) {
132
167
  sendMouseMove();
133
- app.send(MouseClick(id, mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0, getTargetLabel(target), isClickable(target) ? getSelector(id, target) : ''), true);
168
+ app.send(MouseClick(id, mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0, getTargetLabel(target), isClickable(target) && !disableClickmaps ? getSelector(id, target, options) : ''), true);
134
169
  }
135
170
  mouseTarget = null;
136
171
  });
@@ -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/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-beta.1",
4
+ "version": "6.0.0",
5
5
  "keywords": [
6
6
  "logging",
7
7
  "replay"