@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.
Files changed (71) hide show
  1. package/.eslintignore +3 -0
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +22 -18
  4. package/cjs/app/guards.d.ts +11 -12
  5. package/cjs/app/guards.js +2 -1
  6. package/cjs/app/index.d.ts +11 -8
  7. package/cjs/app/index.js +27 -8
  8. package/cjs/app/logger.d.ts +3 -3
  9. package/cjs/app/messages.gen.d.ts +8 -6
  10. package/cjs/app/messages.gen.js +116 -94
  11. package/cjs/app/nodes.d.ts +1 -1
  12. package/cjs/app/observer/iframe_offsets.d.ts +1 -1
  13. package/cjs/app/observer/observer.js +5 -5
  14. package/cjs/app/observer/top_observer.d.ts +2 -2
  15. package/cjs/app/observer/top_observer.js +1 -1
  16. package/cjs/app/session.d.ts +2 -2
  17. package/cjs/app/ticker.d.ts +1 -1
  18. package/cjs/common/interaction.d.ts +5 -5
  19. package/cjs/common/messages.gen.d.ts +104 -86
  20. package/cjs/index.d.ts +6 -2
  21. package/cjs/index.js +7 -5
  22. package/cjs/modules/cssrules.js +1 -1
  23. package/cjs/modules/focus.js +2 -2
  24. package/cjs/modules/fonts.js +2 -2
  25. package/cjs/modules/img.js +6 -5
  26. package/cjs/modules/input.d.ts +1 -1
  27. package/cjs/modules/input.js +15 -23
  28. package/cjs/modules/mouse.js +1 -1
  29. package/cjs/modules/network.d.ts +28 -0
  30. package/cjs/modules/network.js +203 -0
  31. package/cjs/modules/timing.js +7 -3
  32. package/cjs/modules/viewport.js +3 -1
  33. package/cjs/vendors/finder/finder.d.ts +1 -1
  34. package/jest.config.js +11 -0
  35. package/lib/app/guards.d.ts +11 -12
  36. package/lib/app/guards.js +2 -1
  37. package/lib/app/index.d.ts +11 -8
  38. package/lib/app/index.js +28 -9
  39. package/lib/app/logger.d.ts +3 -3
  40. package/lib/app/messages.gen.d.ts +8 -6
  41. package/lib/app/messages.gen.js +107 -87
  42. package/lib/app/nodes.d.ts +1 -1
  43. package/lib/app/observer/iframe_offsets.d.ts +1 -1
  44. package/lib/app/observer/observer.js +5 -5
  45. package/lib/app/observer/top_observer.d.ts +2 -2
  46. package/lib/app/observer/top_observer.js +1 -1
  47. package/lib/app/session.d.ts +2 -2
  48. package/lib/app/ticker.d.ts +1 -1
  49. package/lib/common/interaction.d.ts +5 -5
  50. package/lib/common/messages.gen.d.ts +104 -86
  51. package/lib/common/tsconfig.tsbuildinfo +1 -1
  52. package/lib/index.d.ts +6 -2
  53. package/lib/index.js +8 -6
  54. package/lib/modules/cssrules.js +1 -1
  55. package/lib/modules/focus.js +2 -2
  56. package/lib/modules/fonts.js +2 -2
  57. package/lib/modules/img.js +6 -5
  58. package/lib/modules/input.d.ts +1 -1
  59. package/lib/modules/input.js +15 -23
  60. package/lib/modules/mouse.js +1 -1
  61. package/lib/modules/network.d.ts +28 -0
  62. package/lib/modules/network.js +200 -0
  63. package/lib/modules/timing.js +8 -4
  64. package/lib/modules/viewport.js +3 -1
  65. package/lib/vendors/finder/finder.d.ts +1 -1
  66. package/package.json +8 -3
  67. package/tsconfig-base.json +2 -1
  68. package/cjs/app/messages.d.ts +0 -52
  69. package/cjs/app/messages.js +0 -234
  70. package/lib/app/messages.d.ts +0 -52
  71. 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 declare type Options = Partial<AppOptions & ConsoleOptions & ExceptionOptions & InputOptions & PerformanceOptions & TimingOptions> & {
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(): string | undefined;
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, RawCustomEvent, CustomIssue } from './app/messages.gen.js';
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/setup-or';
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.8',
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(RawCustomEvent(key, payload));
233
+ this.app.send(CustomEvent(key, payload));
232
234
  }
233
235
  }
234
236
  }
@@ -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 (!(hasTag(node, 'STYLE') || hasTag(node, 'style')) || !node.sheet) {
78
+ if (!hasTag(node, 'style') || !node.sheet) {
79
79
  return;
80
80
  }
81
81
  if (node.textContent !== null && node.textContent.trim().length > 0) {
@@ -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, 'BODY')) {
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, 'IFRAME') && elem.contentDocument) {
35
+ while (elem && hasTag(elem, 'iframe') && elem.contentDocument) {
36
36
  elem = elem.contentDocument.activeElement;
37
37
  }
38
38
  if (elem && elem !== elem.ownerDocument.body) {
@@ -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
  }
@@ -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
- return location.origin + url;
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, 'IMG')) {
99
+ if (!hasTag(node, 'img')) {
99
100
  return;
100
101
  }
101
102
  app.nodes.attachNodeListener(node, 'error', () => sendImgError(node));
@@ -1,5 +1,5 @@
1
1
  import type App from '../app/index.js';
2
- declare type TextEditableElement = HTMLInputElement | HTMLTextAreaElement;
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,
@@ -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, 'TEXTAREA')) {
6
+ if (hasTag(node, 'textarea')) {
7
7
  return true;
8
8
  }
9
- if (!hasTag(node, 'INPUT')) {
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, 'INPUT')) {
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, 'LABEL')) {
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, 'LABEL')) {
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, 'SELECT')) {
141
+ if (hasTag(node, 'select')) {
150
142
  sendInputValue(id, node);
151
143
  app.attachEventListener(node, 'change', () => {
152
144
  sendInputValue(id, node);
@@ -78,7 +78,7 @@ export default function (app) {
78
78
  if (dl !== null) {
79
79
  return dl;
80
80
  }
81
- if (hasTag(target, 'INPUT')) {
81
+ if (hasTag(target, 'input')) {
82
82
  return getInputLabel(target);
83
83
  }
84
84
  if (isClickable(target)) {
@@ -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
+ }
@@ -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, 'IMG')) {
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 + performance.timing.navigationStart, 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));
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 { navigationStart, requestStart, responseStart, responseEnd, domContentLoadedEventStart, domContentLoadedEventEnd, loadEventStart, loadEventEnd, } = performance.timing;
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);
@@ -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, document.referrer, navigationStart));
11
+ app.send(SetPageLocation(url, referrer, navigationStart));
11
12
  navigationStart = 0;
13
+ referrer = url;
12
14
  }
13
15
  });
14
16
  const sendSetViewportSize = app.safe(() => {
@@ -1,4 +1,4 @@
1
- export declare type Options = {
1
+ export type Options = {
2
2
  root: Element;
3
3
  idName: (name: string) => boolean;
4
4
  className: (name: string) => boolean;
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.9",
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
- "typescript": "4.6.0-dev.20211126"
46
+ "ts-jest": "^29.0.3",
47
+ "typescript": "^4.9.4"
43
48
  },
44
49
  "dependencies": {
45
50
  "error-stack-parser": "^2.0.6"
@@ -10,5 +10,6 @@
10
10
  "target": "es6",
11
11
  "module": "es6",
12
12
  "moduleResolution": "nodenext"
13
- }
13
+ },
14
+ "exclude": ["**/*.test.ts"]
14
15
  }