@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.
- package/CHANGELOG.md +13 -0
- package/cjs/app/index.d.ts +3 -0
- package/cjs/app/index.js +4 -3
- package/cjs/app/messages.gen.d.ts +6 -1
- package/cjs/app/messages.gen.js +56 -5
- package/cjs/app/nodes.d.ts +1 -0
- package/cjs/app/nodes.js +3 -0
- package/cjs/app/observer/observer.js +7 -1
- package/cjs/app/observer/top_observer.js +2 -0
- package/cjs/app/ticker.d.ts +6 -0
- package/cjs/app/ticker.js +6 -0
- package/cjs/common/messages.gen.d.ts +46 -5
- package/cjs/index.d.ts +2 -0
- package/cjs/index.js +4 -2
- package/cjs/modules/img.js +1 -1
- package/cjs/modules/input.d.ts +2 -2
- package/cjs/modules/input.js +61 -39
- package/cjs/modules/mouse.d.ts +23 -1
- package/cjs/modules/mouse.js +44 -9
- package/cjs/modules/network.d.ts +1 -1
- package/cjs/modules/network.js +81 -10
- package/cjs/modules/selection.d.ts +7 -0
- package/cjs/modules/selection.js +37 -0
- package/cjs/modules/timing.js +3 -1
- package/lib/app/index.d.ts +3 -0
- package/lib/app/index.js +4 -3
- package/lib/app/messages.gen.d.ts +6 -1
- package/lib/app/messages.gen.js +48 -2
- package/lib/app/nodes.d.ts +1 -0
- package/lib/app/nodes.js +3 -0
- package/lib/app/observer/observer.js +8 -2
- package/lib/app/observer/top_observer.js +2 -0
- package/lib/app/ticker.d.ts +6 -0
- package/lib/app/ticker.js +6 -0
- package/lib/common/messages.gen.d.ts +46 -5
- package/lib/common/tsconfig.tsbuildinfo +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.js +4 -2
- package/lib/modules/img.js +1 -1
- package/lib/modules/input.d.ts +2 -2
- package/lib/modules/input.js +63 -41
- package/lib/modules/mouse.d.ts +23 -1
- package/lib/modules/mouse.js +46 -11
- package/lib/modules/network.d.ts +1 -1
- package/lib/modules/network.js +81 -10
- package/lib/modules/selection.d.ts +7 -0
- package/lib/modules/selection.js +35 -0
- package/lib/modules/timing.js +3 -1
- package/package.json +1 -1
package/lib/modules/mouse.js
CHANGED
|
@@ -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:
|
|
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
|
});
|
package/lib/modules/network.d.ts
CHANGED
|
@@ -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 {};
|
package/lib/modules/network.js
CHANGED
|
@@ -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 =
|
|
67
|
-
|
|
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 =
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
// }
|
package/lib/modules/timing.js
CHANGED
|
@@ -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;
|