@openreplay/tracker 4.1.9 → 4.1.10
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/.eslintignore +3 -0
- package/CHANGELOG.md +19 -0
- package/README.md +22 -18
- package/cjs/app/guards.d.ts +11 -12
- package/cjs/app/guards.js +2 -1
- package/cjs/app/index.d.ts +11 -8
- package/cjs/app/index.js +27 -8
- package/cjs/app/logger.d.ts +3 -3
- package/cjs/app/messages.gen.d.ts +8 -6
- package/cjs/app/messages.gen.js +116 -94
- package/cjs/app/nodes.d.ts +1 -1
- package/cjs/app/observer/iframe_offsets.d.ts +1 -1
- package/cjs/app/observer/observer.js +5 -5
- package/cjs/app/observer/top_observer.d.ts +2 -2
- package/cjs/app/observer/top_observer.js +1 -1
- package/cjs/app/session.d.ts +2 -2
- package/cjs/app/ticker.d.ts +1 -1
- package/cjs/common/interaction.d.ts +5 -5
- package/cjs/common/messages.gen.d.ts +104 -86
- package/cjs/index.d.ts +6 -2
- package/cjs/index.js +7 -5
- package/cjs/modules/cssrules.js +1 -1
- package/cjs/modules/focus.js +2 -2
- package/cjs/modules/fonts.js +2 -2
- package/cjs/modules/img.js +6 -5
- package/cjs/modules/input.d.ts +1 -1
- package/cjs/modules/input.js +15 -23
- package/cjs/modules/mouse.js +1 -1
- package/cjs/modules/network.d.ts +28 -0
- package/cjs/modules/network.js +203 -0
- package/cjs/modules/timing.js +7 -3
- package/cjs/modules/viewport.js +3 -1
- package/cjs/vendors/finder/finder.d.ts +1 -1
- package/jest.config.js +11 -0
- package/lib/app/guards.d.ts +11 -12
- package/lib/app/guards.js +2 -1
- package/lib/app/index.d.ts +11 -8
- package/lib/app/index.js +28 -9
- package/lib/app/logger.d.ts +3 -3
- package/lib/app/messages.gen.d.ts +8 -6
- package/lib/app/messages.gen.js +107 -87
- package/lib/app/nodes.d.ts +1 -1
- package/lib/app/observer/iframe_offsets.d.ts +1 -1
- package/lib/app/observer/observer.js +5 -5
- package/lib/app/observer/top_observer.d.ts +2 -2
- package/lib/app/observer/top_observer.js +1 -1
- package/lib/app/session.d.ts +2 -2
- package/lib/app/ticker.d.ts +1 -1
- package/lib/common/interaction.d.ts +5 -5
- package/lib/common/messages.gen.d.ts +104 -86
- package/lib/common/tsconfig.tsbuildinfo +1 -1
- package/lib/index.d.ts +6 -2
- package/lib/index.js +8 -6
- package/lib/modules/cssrules.js +1 -1
- package/lib/modules/focus.js +2 -2
- package/lib/modules/fonts.js +2 -2
- package/lib/modules/img.js +6 -5
- package/lib/modules/input.d.ts +1 -1
- package/lib/modules/input.js +15 -23
- package/lib/modules/mouse.js +1 -1
- package/lib/modules/network.d.ts +28 -0
- package/lib/modules/network.js +200 -0
- package/lib/modules/timing.js +8 -4
- package/lib/modules/viewport.js +3 -1
- package/lib/vendors/finder/finder.d.ts +1 -1
- package/package.json +8 -3
- package/tsconfig-base.json +2 -1
- package/cjs/app/messages.d.ts +0 -52
- package/cjs/app/messages.js +0 -234
- package/lib/app/messages.d.ts +0 -52
- package/lib/app/messages.js +0 -181
package/lib/index.d.ts
CHANGED
|
@@ -9,14 +9,16 @@ import type { Options as ExceptionOptions } from './modules/exception.js';
|
|
|
9
9
|
import type { Options as InputOptions } from './modules/input.js';
|
|
10
10
|
import type { Options as PerformanceOptions } from './modules/performance.js';
|
|
11
11
|
import type { Options as TimingOptions } from './modules/timing.js';
|
|
12
|
+
import type { Options as NetworkOptions } from './modules/network.js';
|
|
12
13
|
import type { StartOptions } from './app/index.js';
|
|
13
14
|
import type { StartPromiseReturn } from './app/index.js';
|
|
14
|
-
export
|
|
15
|
+
export type Options = Partial<AppOptions & ConsoleOptions & ExceptionOptions & InputOptions & PerformanceOptions & TimingOptions> & {
|
|
15
16
|
projectID?: number;
|
|
16
17
|
projectKey: string;
|
|
17
18
|
sessionToken?: string;
|
|
18
19
|
respectDoNotTrack?: boolean;
|
|
19
20
|
autoResetOnWindowOpen?: boolean;
|
|
21
|
+
network?: NetworkOptions;
|
|
20
22
|
__DISABLE_SECURE_MODE?: boolean;
|
|
21
23
|
};
|
|
22
24
|
export default class API {
|
|
@@ -30,7 +32,9 @@ export default class API {
|
|
|
30
32
|
getSessionToken(): string | null | undefined;
|
|
31
33
|
getSessionID(): string | null | undefined;
|
|
32
34
|
sessionID(): string | null | undefined;
|
|
33
|
-
getSessionURL(
|
|
35
|
+
getSessionURL(options?: {
|
|
36
|
+
withCurrentTime?: boolean;
|
|
37
|
+
}): string | undefined;
|
|
34
38
|
setUserID(id: string): void;
|
|
35
39
|
userID(id: string): void;
|
|
36
40
|
setUserAnonymousID(id: string): void;
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import App, { DEFAULT_INGEST_POINT } from './app/index.js';
|
|
2
2
|
export { default as App } from './app/index.js';
|
|
3
|
-
import { UserAnonymousID,
|
|
3
|
+
import { UserAnonymousID, CustomEvent, CustomIssue } from './app/messages.gen.js';
|
|
4
4
|
import * as _Messages from './app/messages.gen.js';
|
|
5
5
|
export const Messages = _Messages;
|
|
6
6
|
export { SanitizeLevel } from './app/sanitizer.js';
|
|
@@ -17,9 +17,10 @@ import Viewport from './modules/viewport.js';
|
|
|
17
17
|
import CSSRules from './modules/cssrules.js';
|
|
18
18
|
import Focus from './modules/focus.js';
|
|
19
19
|
import Fonts from './modules/fonts.js';
|
|
20
|
+
import Network from './modules/network.js';
|
|
20
21
|
import ConstructedStyleSheets from './modules/constructedStyleSheets.js';
|
|
21
22
|
import { IN_BROWSER, deprecationWarn, DOCS_HOST } from './utils.js';
|
|
22
|
-
const DOCS_SETUP = '/installation/
|
|
23
|
+
const DOCS_SETUP = '/installation/javascript-sdk';
|
|
23
24
|
function processOptions(obj) {
|
|
24
25
|
if (obj == null) {
|
|
25
26
|
console.error(`OpenReplay: invalid options argument type. Please, check documentation on ${DOCS_HOST}${DOCS_SETUP}`);
|
|
@@ -109,6 +110,7 @@ export default class API {
|
|
|
109
110
|
Scroll(app);
|
|
110
111
|
Focus(app);
|
|
111
112
|
Fonts(app);
|
|
113
|
+
Network(app, options.network);
|
|
112
114
|
window.__OPENREPLAY__ = this;
|
|
113
115
|
if (options.autoResetOnWindowOpen) {
|
|
114
116
|
const wOpen = window.open;
|
|
@@ -133,7 +135,7 @@ export default class API {
|
|
|
133
135
|
// no-cors issue only with text/plain or not-set Content-Type
|
|
134
136
|
// req.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
|
135
137
|
req.send(JSON.stringify({
|
|
136
|
-
trackerVersion: '4.1.
|
|
138
|
+
trackerVersion: '4.1.10',
|
|
137
139
|
projectKey: options.projectKey,
|
|
138
140
|
doNotTrack,
|
|
139
141
|
// TODO: add precise reason (an exact API missing)
|
|
@@ -183,11 +185,11 @@ export default class API {
|
|
|
183
185
|
deprecationWarn("'sessionID' method", "'getSessionID' method", '/');
|
|
184
186
|
return this.getSessionID();
|
|
185
187
|
}
|
|
186
|
-
getSessionURL() {
|
|
188
|
+
getSessionURL(options) {
|
|
187
189
|
if (this.app === null) {
|
|
188
190
|
return undefined;
|
|
189
191
|
}
|
|
190
|
-
return this.app.getSessionURL();
|
|
192
|
+
return this.app.getSessionURL(options);
|
|
191
193
|
}
|
|
192
194
|
setUserID(id) {
|
|
193
195
|
if (typeof id === 'string' && this.app !== null) {
|
|
@@ -228,7 +230,7 @@ export default class API {
|
|
|
228
230
|
catch (e) {
|
|
229
231
|
return;
|
|
230
232
|
}
|
|
231
|
-
this.app.send(
|
|
233
|
+
this.app.send(CustomEvent(key, payload));
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
}
|
package/lib/modules/cssrules.js
CHANGED
|
@@ -75,7 +75,7 @@ export default function (app) {
|
|
|
75
75
|
patchContext(window);
|
|
76
76
|
app.observer.attachContextCallback(patchContext);
|
|
77
77
|
app.nodes.attachNodeCallback((node) => {
|
|
78
|
-
if (!
|
|
78
|
+
if (!hasTag(node, 'style') || !node.sheet) {
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
81
|
if (node.textContent !== null && node.textContent.trim().length > 0) {
|
package/lib/modules/focus.js
CHANGED
|
@@ -9,7 +9,7 @@ export default function (app) {
|
|
|
9
9
|
}
|
|
10
10
|
let blurred = false;
|
|
11
11
|
app.nodes.attachNodeCallback((node) => {
|
|
12
|
-
if (!hasTag(node, '
|
|
12
|
+
if (!hasTag(node, 'body')) {
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
app.nodes.attachNodeListener(node, 'focus', (e) => {
|
|
@@ -32,7 +32,7 @@ export default function (app) {
|
|
|
32
32
|
});
|
|
33
33
|
app.attachStartCallback(() => {
|
|
34
34
|
let elem = document.activeElement;
|
|
35
|
-
while (elem && hasTag(elem, '
|
|
35
|
+
while (elem && hasTag(elem, 'iframe') && elem.contentDocument) {
|
|
36
36
|
elem = elem.contentDocument.activeElement;
|
|
37
37
|
}
|
|
38
38
|
if (elem && elem !== elem.ownerDocument.body) {
|
package/lib/modules/fonts.js
CHANGED
|
@@ -35,7 +35,7 @@ export default function (app) {
|
|
|
35
35
|
};
|
|
36
36
|
app.observer.attachContextCallback(patchWindow);
|
|
37
37
|
patchWindow(window);
|
|
38
|
-
app.nodes.attachNodeCallback((node) => {
|
|
38
|
+
app.nodes.attachNodeCallback(app.safe((node) => {
|
|
39
39
|
if (!isDocument(node)) {
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
@@ -50,5 +50,5 @@ export default function (app) {
|
|
|
50
50
|
ffDataArr.forEach((ffData) => {
|
|
51
51
|
app.send(LoadFontFace(parentID, ...ffData));
|
|
52
52
|
});
|
|
53
|
-
});
|
|
53
|
+
}));
|
|
54
54
|
}
|
package/lib/modules/img.js
CHANGED
|
@@ -3,15 +3,16 @@ import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from '../a
|
|
|
3
3
|
import { hasTag } from '../app/guards.js';
|
|
4
4
|
function resolveURL(url, location = document.location) {
|
|
5
5
|
url = url.trim();
|
|
6
|
-
if (url.startsWith('
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
else if (url.startsWith('http://') ||
|
|
6
|
+
if (url.startsWith('//') ||
|
|
7
|
+
url.startsWith('http://') ||
|
|
10
8
|
url.startsWith('https://') ||
|
|
11
9
|
url.startsWith('data:') // any other possible value here? https://bugzilla.mozilla.org/show_bug.cgi?id=1758035
|
|
12
10
|
) {
|
|
13
11
|
return url;
|
|
14
12
|
}
|
|
13
|
+
else if (url.startsWith('/')) {
|
|
14
|
+
return location.origin + url;
|
|
15
|
+
}
|
|
15
16
|
else {
|
|
16
17
|
return location.origin + location.pathname + url;
|
|
17
18
|
}
|
|
@@ -95,7 +96,7 @@ export default function (app) {
|
|
|
95
96
|
observer.disconnect();
|
|
96
97
|
});
|
|
97
98
|
app.nodes.attachNodeCallback((node) => {
|
|
98
|
-
if (!hasTag(node, '
|
|
99
|
+
if (!hasTag(node, 'img')) {
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
app.nodes.attachNodeListener(node, 'error', () => sendImgError(node));
|
package/lib/modules/input.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type App from '../app/index.js';
|
|
2
|
-
|
|
2
|
+
type TextEditableElement = HTMLInputElement | HTMLTextAreaElement;
|
|
3
3
|
export declare function getInputLabel(node: TextEditableElement): string;
|
|
4
4
|
export declare const enum InputMode {
|
|
5
5
|
Plain = 0,
|
package/lib/modules/input.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
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
|
-
const INPUT_TYPES = ['text', 'password', 'email', 'search', 'number', 'range', 'date'];
|
|
4
|
+
const INPUT_TYPES = ['text', 'password', 'email', 'search', 'number', 'range', 'date', 'tel'];
|
|
5
5
|
function isTextEditable(node) {
|
|
6
|
-
if (hasTag(node, '
|
|
6
|
+
if (hasTag(node, 'textarea')) {
|
|
7
7
|
return true;
|
|
8
8
|
}
|
|
9
|
-
if (!hasTag(node, '
|
|
9
|
+
if (!hasTag(node, 'input')) {
|
|
10
10
|
return false;
|
|
11
11
|
}
|
|
12
12
|
return INPUT_TYPES.includes(node.type);
|
|
13
13
|
}
|
|
14
14
|
function isCheckable(node) {
|
|
15
|
-
if (!hasTag(node, '
|
|
15
|
+
if (!hasTag(node, 'input')) {
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
const type = node.type;
|
|
@@ -22,7 +22,7 @@ const labelElementFor = IN_BROWSER && 'labels' in HTMLInputElement.prototype
|
|
|
22
22
|
? (node) => {
|
|
23
23
|
let p = node;
|
|
24
24
|
while ((p = p.parentNode) !== null) {
|
|
25
|
-
if (hasTag(p, '
|
|
25
|
+
if (hasTag(p, 'label')) {
|
|
26
26
|
return p;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -34,7 +34,7 @@ const labelElementFor = IN_BROWSER && 'labels' in HTMLInputElement.prototype
|
|
|
34
34
|
: (node) => {
|
|
35
35
|
let p = node;
|
|
36
36
|
while ((p = p.parentNode) !== null) {
|
|
37
|
-
if (hasTag(p, '
|
|
37
|
+
if (hasTag(p, 'label')) {
|
|
38
38
|
return p;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -64,7 +64,7 @@ export default function (app, opts) {
|
|
|
64
64
|
const options = Object.assign({
|
|
65
65
|
obscureInputNumbers: true,
|
|
66
66
|
obscureInputEmails: true,
|
|
67
|
-
defaultInputMode: 0 /* Plain */,
|
|
67
|
+
defaultInputMode: 0 /* InputMode.Plain */,
|
|
68
68
|
obscureInputDates: false,
|
|
69
69
|
}, opts);
|
|
70
70
|
function sendInputTarget(id, node) {
|
|
@@ -77,22 +77,22 @@ export default function (app, opts) {
|
|
|
77
77
|
let value = node.value;
|
|
78
78
|
let inputMode = options.defaultInputMode;
|
|
79
79
|
if (node.type === 'password' || app.sanitizer.isHidden(id)) {
|
|
80
|
-
inputMode = 2 /* Hidden */;
|
|
80
|
+
inputMode = 2 /* InputMode.Hidden */;
|
|
81
81
|
}
|
|
82
82
|
else if (app.sanitizer.isObscured(id) ||
|
|
83
|
-
(inputMode === 0 /* Plain */ &&
|
|
83
|
+
(inputMode === 0 /* InputMode.Plain */ &&
|
|
84
84
|
((options.obscureInputNumbers && node.type !== 'date' && /\d\d\d\d/.test(value)) ||
|
|
85
85
|
(options.obscureInputDates && node.type === 'date') ||
|
|
86
86
|
(options.obscureInputEmails && (node.type === 'email' || !!~value.indexOf('@')))))) {
|
|
87
|
-
inputMode = 1 /* Obscured */;
|
|
87
|
+
inputMode = 1 /* InputMode.Obscured */;
|
|
88
88
|
}
|
|
89
89
|
let mask = 0;
|
|
90
90
|
switch (inputMode) {
|
|
91
|
-
case 2 /* Hidden */:
|
|
91
|
+
case 2 /* InputMode.Hidden */:
|
|
92
92
|
mask = -1;
|
|
93
93
|
value = '';
|
|
94
94
|
break;
|
|
95
|
-
case 1 /* Obscured */:
|
|
95
|
+
case 1 /* InputMode.Obscured */:
|
|
96
96
|
mask = value.length;
|
|
97
97
|
value = '';
|
|
98
98
|
break;
|
|
@@ -111,11 +111,7 @@ export default function (app, opts) {
|
|
|
111
111
|
inputValues.forEach((value, id) => {
|
|
112
112
|
const node = app.nodes.getNode(id);
|
|
113
113
|
if (!node)
|
|
114
|
-
return;
|
|
115
|
-
if (!isTextEditable(node)) {
|
|
116
|
-
inputValues.delete(id);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
114
|
+
return inputValues.delete(id);
|
|
119
115
|
if (value !== node.value) {
|
|
120
116
|
inputValues.set(id, node.value);
|
|
121
117
|
if (!registeredTargets.has(id)) {
|
|
@@ -128,11 +124,7 @@ export default function (app, opts) {
|
|
|
128
124
|
checkableValues.forEach((checked, id) => {
|
|
129
125
|
const node = app.nodes.getNode(id);
|
|
130
126
|
if (!node)
|
|
131
|
-
return;
|
|
132
|
-
if (!isCheckable(node)) {
|
|
133
|
-
checkableValues.delete(id);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
127
|
+
return checkableValues.delete(id);
|
|
136
128
|
if (checked !== node.checked) {
|
|
137
129
|
checkableValues.set(id, node.checked);
|
|
138
130
|
app.send(SetInputChecked(id, node.checked));
|
|
@@ -146,7 +138,7 @@ export default function (app, opts) {
|
|
|
146
138
|
return;
|
|
147
139
|
}
|
|
148
140
|
// TODO: support multiple select (?): use selectedOptions; Need send target?
|
|
149
|
-
if (hasTag(node, '
|
|
141
|
+
if (hasTag(node, 'select')) {
|
|
150
142
|
sendInputValue(id, node);
|
|
151
143
|
app.attachEventListener(node, 'change', () => {
|
|
152
144
|
sendInputValue(id, node);
|
package/lib/modules/mouse.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type App from '../app/index.js';
|
|
2
|
+
type XHRRequestBody = Parameters<XMLHttpRequest['send']>[0];
|
|
3
|
+
type FetchRequestBody = RequestInit['body'];
|
|
4
|
+
interface RequestData {
|
|
5
|
+
body: XHRRequestBody | FetchRequestBody;
|
|
6
|
+
headers: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
interface ResponseData {
|
|
9
|
+
body: any;
|
|
10
|
+
headers: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
interface RequestResponseData {
|
|
13
|
+
readonly status: number;
|
|
14
|
+
readonly method: string;
|
|
15
|
+
url: string;
|
|
16
|
+
request: RequestData;
|
|
17
|
+
response: ResponseData;
|
|
18
|
+
}
|
|
19
|
+
type Sanitizer = (data: RequestResponseData) => RequestResponseData | null;
|
|
20
|
+
export interface Options {
|
|
21
|
+
sessionTokenHeader: string | boolean;
|
|
22
|
+
failuresOnly: boolean;
|
|
23
|
+
ignoreHeaders: Array<string> | boolean;
|
|
24
|
+
capturePayload: boolean;
|
|
25
|
+
sanitizer?: Sanitizer;
|
|
26
|
+
}
|
|
27
|
+
export default function (app: App, opts?: Partial<Options>): void;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { NetworkRequest } from '../app/messages.gen.js';
|
|
2
|
+
import { getTimeOrigin } from '../utils.js';
|
|
3
|
+
function getXHRRequestDataObject(xhr) {
|
|
4
|
+
// @ts-ignore this is 3x faster than using Map<XHR, XHRRequestData>
|
|
5
|
+
if (!xhr.__or_req_data__) {
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
xhr.__or_req_data__ = { body: undefined, headers: {} };
|
|
8
|
+
}
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
return xhr.__or_req_data__;
|
|
11
|
+
}
|
|
12
|
+
function strMethod(method) {
|
|
13
|
+
return typeof method === 'string' ? method.toUpperCase() : 'GET';
|
|
14
|
+
}
|
|
15
|
+
export default function (app, opts = {}) {
|
|
16
|
+
const options = Object.assign({
|
|
17
|
+
failuresOnly: false,
|
|
18
|
+
ignoreHeaders: ['Cookie', 'Set-Cookie', 'Authorization'],
|
|
19
|
+
capturePayload: false,
|
|
20
|
+
sessionTokenHeader: false,
|
|
21
|
+
}, opts);
|
|
22
|
+
const ignoreHeaders = options.ignoreHeaders;
|
|
23
|
+
const isHIgnored = Array.isArray(ignoreHeaders)
|
|
24
|
+
? (name) => ignoreHeaders.includes(name)
|
|
25
|
+
: () => ignoreHeaders;
|
|
26
|
+
const stHeader = options.sessionTokenHeader === true ? 'X-OpenReplay-SessionToken' : options.sessionTokenHeader;
|
|
27
|
+
function setSessionTokenHeader(setRequestHeader) {
|
|
28
|
+
if (stHeader) {
|
|
29
|
+
const sessionToken = app.getSessionToken();
|
|
30
|
+
if (sessionToken) {
|
|
31
|
+
app.safe(setRequestHeader)(stHeader, sessionToken);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function sanitize(reqResInfo) {
|
|
36
|
+
if (!options.capturePayload) {
|
|
37
|
+
delete reqResInfo.request.body;
|
|
38
|
+
delete reqResInfo.response.body;
|
|
39
|
+
}
|
|
40
|
+
if (options.sanitizer) {
|
|
41
|
+
const resBody = reqResInfo.response.body;
|
|
42
|
+
if (typeof resBody === 'string') {
|
|
43
|
+
// Parse response in order to have handy view in sanitisation function
|
|
44
|
+
try {
|
|
45
|
+
reqResInfo.response.body = JSON.parse(resBody);
|
|
46
|
+
}
|
|
47
|
+
catch (_a) { }
|
|
48
|
+
}
|
|
49
|
+
return options.sanitizer(reqResInfo);
|
|
50
|
+
}
|
|
51
|
+
return reqResInfo;
|
|
52
|
+
}
|
|
53
|
+
function stringify(r) {
|
|
54
|
+
if (r && typeof r.body !== 'string') {
|
|
55
|
+
try {
|
|
56
|
+
r.body = JSON.stringify(r.body);
|
|
57
|
+
}
|
|
58
|
+
catch (_a) {
|
|
59
|
+
r.body = '<unable to stringify>';
|
|
60
|
+
app.notify.warn("Openreplay fetch couldn't stringify body:", r.body);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return JSON.stringify(r);
|
|
64
|
+
}
|
|
65
|
+
/* ====== Fetch ====== */
|
|
66
|
+
const origFetch = window.fetch.bind(window);
|
|
67
|
+
window.fetch = (input, init = {}) => {
|
|
68
|
+
if (!(typeof input === 'string' || input instanceof URL) || app.isServiceURL(String(input))) {
|
|
69
|
+
return origFetch(input, init);
|
|
70
|
+
}
|
|
71
|
+
setSessionTokenHeader(function (name, value) {
|
|
72
|
+
if (init.headers === undefined) {
|
|
73
|
+
init.headers = {};
|
|
74
|
+
}
|
|
75
|
+
if (init.headers instanceof Headers) {
|
|
76
|
+
init.headers.append(name, value);
|
|
77
|
+
}
|
|
78
|
+
else if (Array.isArray(init.headers)) {
|
|
79
|
+
init.headers.push([name, value]);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
init.headers[name] = value;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const startTime = performance.now();
|
|
86
|
+
return origFetch(input, init).then((response) => {
|
|
87
|
+
const duration = performance.now() - startTime;
|
|
88
|
+
if (options.failuresOnly && response.status < 400) {
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
const r = response.clone();
|
|
92
|
+
r.text()
|
|
93
|
+
.then((text) => {
|
|
94
|
+
const reqHs = {};
|
|
95
|
+
const resHs = {};
|
|
96
|
+
if (ignoreHeaders !== true) {
|
|
97
|
+
// request headers
|
|
98
|
+
const writeReqHeader = ([n, v]) => {
|
|
99
|
+
if (!isHIgnored(n)) {
|
|
100
|
+
reqHs[n] = v;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
if (init.headers instanceof Headers) {
|
|
104
|
+
init.headers.forEach((v, n) => writeReqHeader([n, v]));
|
|
105
|
+
}
|
|
106
|
+
else if (Array.isArray(init.headers)) {
|
|
107
|
+
init.headers.forEach(writeReqHeader);
|
|
108
|
+
}
|
|
109
|
+
else if (typeof init.headers === 'object') {
|
|
110
|
+
Object.entries(init.headers).forEach(writeReqHeader);
|
|
111
|
+
}
|
|
112
|
+
// response headers
|
|
113
|
+
r.headers.forEach((v, n) => {
|
|
114
|
+
if (!isHIgnored(n))
|
|
115
|
+
resHs[n] = v;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const method = strMethod(init.method);
|
|
119
|
+
const reqResInfo = sanitize({
|
|
120
|
+
url: String(input),
|
|
121
|
+
method,
|
|
122
|
+
status: r.status,
|
|
123
|
+
request: {
|
|
124
|
+
headers: reqHs,
|
|
125
|
+
body: init.body,
|
|
126
|
+
},
|
|
127
|
+
response: {
|
|
128
|
+
headers: resHs,
|
|
129
|
+
body: text,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
if (!reqResInfo) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
app.send(NetworkRequest('fetch', method, String(reqResInfo.url), stringify(reqResInfo.request), stringify(reqResInfo.response), r.status, startTime + getTimeOrigin(), duration));
|
|
136
|
+
})
|
|
137
|
+
.catch((e) => app.debug.error('Could not process Fetch response:', e));
|
|
138
|
+
return response;
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
/* ====== <> ====== */
|
|
142
|
+
/* ====== XHR ====== */
|
|
143
|
+
const nativeOpen = XMLHttpRequest.prototype.open;
|
|
144
|
+
XMLHttpRequest.prototype.open = function (initMethod, url) {
|
|
145
|
+
const xhr = this;
|
|
146
|
+
setSessionTokenHeader((name, value) => xhr.setRequestHeader(name, value));
|
|
147
|
+
let startTime = 0;
|
|
148
|
+
xhr.addEventListener('loadstart', (e) => {
|
|
149
|
+
startTime = e.timeStamp;
|
|
150
|
+
});
|
|
151
|
+
xhr.addEventListener('load', app.safe((e) => {
|
|
152
|
+
const { headers: reqHs, body: reqBody } = getXHRRequestDataObject(xhr);
|
|
153
|
+
const duration = startTime > 0 ? e.timeStamp - startTime : 0;
|
|
154
|
+
const hString = ignoreHeaders ? '' : xhr.getAllResponseHeaders(); // might be null (though only if no response received though)
|
|
155
|
+
const resHs = hString
|
|
156
|
+
? hString
|
|
157
|
+
.split('\r\n')
|
|
158
|
+
.map((h) => h.split(':'))
|
|
159
|
+
.filter((entry) => !isHIgnored(entry[0]))
|
|
160
|
+
.reduce((hds, [name, value]) => (Object.assign(Object.assign({}, hds), { [name]: value })), {})
|
|
161
|
+
: {};
|
|
162
|
+
const method = strMethod(initMethod);
|
|
163
|
+
const reqResInfo = sanitize({
|
|
164
|
+
url: String(url),
|
|
165
|
+
method,
|
|
166
|
+
status: xhr.status,
|
|
167
|
+
request: {
|
|
168
|
+
headers: reqHs,
|
|
169
|
+
body: reqBody,
|
|
170
|
+
},
|
|
171
|
+
response: {
|
|
172
|
+
headers: resHs,
|
|
173
|
+
body: xhr.response,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
if (!reqResInfo) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
app.send(NetworkRequest('xhr', method, String(reqResInfo.url), stringify(reqResInfo.request), stringify(reqResInfo.response), xhr.status, startTime + getTimeOrigin(), duration));
|
|
180
|
+
}));
|
|
181
|
+
//TODO: handle error (though it has no Error API nor any useful information)
|
|
182
|
+
//xhr.addEventListener('error', (e) => {})
|
|
183
|
+
return nativeOpen.apply(this, arguments);
|
|
184
|
+
};
|
|
185
|
+
const nativeSend = XMLHttpRequest.prototype.send;
|
|
186
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
187
|
+
const rdo = getXHRRequestDataObject(this);
|
|
188
|
+
rdo.body = body;
|
|
189
|
+
return nativeSend.apply(this, arguments);
|
|
190
|
+
};
|
|
191
|
+
const nativeSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
192
|
+
XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
|
|
193
|
+
if (!isHIgnored(name)) {
|
|
194
|
+
const rdo = getXHRRequestDataObject(this);
|
|
195
|
+
rdo.headers[name] = value;
|
|
196
|
+
}
|
|
197
|
+
return nativeSetRequestHeader.apply(this, arguments);
|
|
198
|
+
};
|
|
199
|
+
/* ====== <> ====== */
|
|
200
|
+
}
|
package/lib/modules/timing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hasTag } from '../app/guards.js';
|
|
2
|
-
import { isURL } from '../utils.js';
|
|
2
|
+
import { isURL, getTimeOrigin } from '../utils.js';
|
|
3
3
|
import { ResourceTiming, PageLoadTiming, PageRenderTiming } from '../app/messages.gen.js';
|
|
4
4
|
function getPaintBlocks(resources) {
|
|
5
5
|
const paintBlocks = [];
|
|
@@ -8,7 +8,7 @@ function getPaintBlocks(resources) {
|
|
|
8
8
|
for (let i = 0; i < elements.length; i++) {
|
|
9
9
|
const element = elements[i];
|
|
10
10
|
let src = '';
|
|
11
|
-
if (hasTag(element, '
|
|
11
|
+
if (hasTag(element, 'img')) {
|
|
12
12
|
src = element.currentSrc || element.src;
|
|
13
13
|
}
|
|
14
14
|
if (!src) {
|
|
@@ -73,7 +73,7 @@ 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 +
|
|
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));
|
|
77
77
|
}
|
|
78
78
|
const observer = new PerformanceObserver((list) => list.getEntries().forEach(resourceTiming));
|
|
79
79
|
let prevSessionID;
|
|
@@ -110,7 +110,11 @@ export default function (app, opts) {
|
|
|
110
110
|
}
|
|
111
111
|
if (performance.timing.loadEventEnd || performance.now() > 30000) {
|
|
112
112
|
pageLoadTimingSent = true;
|
|
113
|
-
const {
|
|
113
|
+
const {
|
|
114
|
+
// should be ok to use here, (https://github.com/mdn/content/issues/4713)
|
|
115
|
+
// since it is compared with the values obtained on the page load (before any possible sleep state)
|
|
116
|
+
// deprecated though
|
|
117
|
+
navigationStart, requestStart, responseStart, responseEnd, domContentLoadedEventStart, domContentLoadedEventEnd, loadEventStart, loadEventEnd, } = performance.timing;
|
|
114
118
|
app.send(PageLoadTiming(requestStart - navigationStart || 0, responseStart - navigationStart || 0, responseEnd - navigationStart || 0, domContentLoadedEventStart - navigationStart || 0, domContentLoadedEventEnd - navigationStart || 0, loadEventStart - navigationStart || 0, loadEventEnd - navigationStart || 0, firstPaint, firstContentfulPaint));
|
|
115
119
|
}
|
|
116
120
|
}, 30);
|
package/lib/modules/viewport.js
CHANGED
|
@@ -3,12 +3,14 @@ import { SetPageLocation, SetViewportSize, SetPageVisibility } from '../app/mess
|
|
|
3
3
|
export default function (app) {
|
|
4
4
|
let url, width, height;
|
|
5
5
|
let navigationStart;
|
|
6
|
+
let referrer = document.referrer;
|
|
6
7
|
const sendSetPageLocation = app.safe(() => {
|
|
7
8
|
const { URL } = document;
|
|
8
9
|
if (URL !== url) {
|
|
9
10
|
url = URL;
|
|
10
|
-
app.send(SetPageLocation(url,
|
|
11
|
+
app.send(SetPageLocation(url, referrer, navigationStart));
|
|
11
12
|
navigationStart = 0;
|
|
13
|
+
referrer = url;
|
|
12
14
|
}
|
|
13
15
|
});
|
|
14
16
|
const sendSetViewportSize = app.safe(() => {
|
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.1.
|
|
4
|
+
"version": "4.1.10",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"logging",
|
|
7
7
|
"replay"
|
|
@@ -21,10 +21,12 @@
|
|
|
21
21
|
"compile": "node --experimental-modules --experimental-json-modules scripts/compile.cjs",
|
|
22
22
|
"build": "npm run clean && npm run tscRun && npm run rollup && npm run compile",
|
|
23
23
|
"prepare": "cd ../../ && husky install tracker/.husky/",
|
|
24
|
-
"lint-front": "lint-staged"
|
|
24
|
+
"lint-front": "lint-staged",
|
|
25
|
+
"test": "jest"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
28
|
"@babel/core": "^7.10.2",
|
|
29
|
+
"@jest/globals": "^29.3.1",
|
|
28
30
|
"@rollup/plugin-babel": "^5.0.3",
|
|
29
31
|
"@rollup/plugin-node-resolve": "^10.0.0",
|
|
30
32
|
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
|
@@ -33,13 +35,16 @@
|
|
|
33
35
|
"eslint-config-prettier": "^8.5.0",
|
|
34
36
|
"eslint-plugin-prettier": "^4.2.1",
|
|
35
37
|
"husky": "^8.0.1",
|
|
38
|
+
"jest": "^29.3.1",
|
|
39
|
+
"jest-environment-jsdom": "^29.3.1",
|
|
36
40
|
"lint-staged": "^13.0.3",
|
|
37
41
|
"prettier": "^2.7.1",
|
|
38
42
|
"replace-in-files": "^2.0.3",
|
|
39
43
|
"rollup": "^2.17.0",
|
|
40
44
|
"rollup-plugin-terser": "^6.1.0",
|
|
41
45
|
"semver": "^6.3.0",
|
|
42
|
-
"
|
|
46
|
+
"ts-jest": "^29.0.3",
|
|
47
|
+
"typescript": "^4.9.4"
|
|
43
48
|
},
|
|
44
49
|
"dependencies": {
|
|
45
50
|
"error-stack-parser": "^2.0.6"
|