@openreplay/tracker 3.4.15 → 3.4.17

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.
@@ -1,15 +1,18 @@
1
- import Observer, { isInstance } from "./observer.js";
1
+ import Observer from "./observer.js";
2
+ import { isInstance } from "../context.js";
2
3
  import IFrameObserver from "./iframe_observer.js";
3
4
  import ShadowRootObserver from "./shadow_root_observer.js";
4
5
  import { CreateDocument } from "../../messages/index.js";
5
- const attachShadowNativeFn = Element.prototype.attachShadow;
6
+ import { IN_BROWSER } from '../../utils.js';
7
+ const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () => new ShadowRoot();
6
8
  export default class TopObserver extends Observer {
7
9
  constructor(app, options) {
8
- super(app, Object.assign({
9
- captureIFrames: false
10
- }, options));
10
+ super(app);
11
11
  this.iframeObservers = [];
12
12
  this.shadowRootObservers = [];
13
+ this.options = Object.assign({
14
+ captureIFrames: false
15
+ }, options);
13
16
  // IFrames
14
17
  this.app.nodes.attachNodeCallback(node => {
15
18
  if (isInstance(node, HTMLIFrameElement) &&
@@ -38,7 +41,7 @@ export default class TopObserver extends Observer {
38
41
  if (!context) {
39
42
  return;
40
43
  }
41
- const observer = new IFrameObserver(this.app, this.options, context);
44
+ const observer = new IFrameObserver(this.app, context);
42
45
  this.iframeObservers.push(observer);
43
46
  observer.observe(iframe);
44
47
  });
@@ -46,7 +49,7 @@ export default class TopObserver extends Observer {
46
49
  handle();
47
50
  }
48
51
  handleShadowRoot(shRoot) {
49
- const observer = new ShadowRootObserver(this.app, this.options, this.context);
52
+ const observer = new ShadowRootObserver(this.app, this.context);
50
53
  this.shadowRootObservers.push(observer);
51
54
  observer.observe(shRoot.host);
52
55
  }
@@ -0,0 +1,16 @@
1
+ import App from "./index.js";
2
+ export interface Options {
3
+ obscureTextEmails: boolean;
4
+ obscureTextNumbers: boolean;
5
+ }
6
+ export default class Sanitizer {
7
+ private readonly app;
8
+ private readonly masked;
9
+ private readonly options;
10
+ constructor(app: App, options: Partial<Options>);
11
+ handleNode(id: number, parentID: number, node: Node): void;
12
+ sanitize(id: number, data: string): string;
13
+ isMasked(id: number): boolean;
14
+ getInnerTextSecure(el: HTMLElement): string;
15
+ clear(): void;
16
+ }
@@ -1 +1,44 @@
1
- "use strict";
1
+ import { stars, hasOpenreplayAttribute } from "../utils.js";
2
+ import { isInstance } from "./context.js";
3
+ export default class Sanitizer {
4
+ constructor(app, options) {
5
+ this.app = app;
6
+ this.masked = new Set();
7
+ this.options = Object.assign({
8
+ obscureTextEmails: true,
9
+ obscureTextNumbers: false,
10
+ }, options);
11
+ }
12
+ handleNode(id, parentID, node) {
13
+ if (this.masked.has(parentID) ||
14
+ (isInstance(node, Element) && hasOpenreplayAttribute(node, 'masked'))) {
15
+ this.masked.add(id);
16
+ }
17
+ }
18
+ sanitize(id, data) {
19
+ if (this.masked.has(id)) {
20
+ // TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
21
+ return data.trim().replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█');
22
+ }
23
+ if (this.options.obscureTextNumbers) {
24
+ data = data.replace(/\d/g, '0');
25
+ }
26
+ if (this.options.obscureTextEmails) {
27
+ data = data.replace(/([^\s]+)@([^\s]+)\.([^\s]+)/g, (...f) => stars(f[1]) + '@' + stars(f[2]) + '.' + stars(f[3]));
28
+ }
29
+ return data;
30
+ }
31
+ isMasked(id) {
32
+ return this.masked.has(id);
33
+ }
34
+ getInnerTextSecure(el) {
35
+ const id = this.app.nodes.getID(el);
36
+ if (!id) {
37
+ return '';
38
+ }
39
+ return this.sanitize(id, el.innerText);
40
+ }
41
+ clear() {
42
+ this.masked.clear();
43
+ }
44
+ }
package/lib/index.d.ts CHANGED
@@ -2,18 +2,20 @@ import App from "./app/index.js";
2
2
  export { default as App } from './app/index.js';
3
3
  import * as _Messages from "./messages/index.js";
4
4
  export declare const Messages: typeof _Messages;
5
- import { Options as AppOptions } from "./app/index.js";
6
- import { Options as ConsoleOptions } from "./modules/console.js";
7
- import { Options as ExceptionOptions } from "./modules/exception.js";
8
- import { Options as InputOptions } from "./modules/input.js";
9
- import { Options as PerformanceOptions } from "./modules/performance.js";
10
- import { Options as TimingOptions } from "./modules/timing.js";
11
- export type { OnStartInfo } from './app/index.js';
5
+ import type { Options as AppOptions } from "./app/index.js";
6
+ import type { Options as ConsoleOptions } from "./modules/console.js";
7
+ import type { Options as ExceptionOptions } from "./modules/exception.js";
8
+ import type { Options as InputOptions } from "./modules/input.js";
9
+ import type { Options as PerformanceOptions } from "./modules/performance.js";
10
+ import type { Options as TimingOptions } from "./modules/timing.js";
11
+ import type { StartOptions } from './app/index.js';
12
+ import type { OnStartInfo } from './app/index.js';
12
13
  export declare type Options = Partial<AppOptions & ConsoleOptions & ExceptionOptions & InputOptions & PerformanceOptions & TimingOptions> & {
13
14
  projectID?: number;
14
15
  projectKey: string;
15
16
  sessionToken?: string;
16
17
  respectDoNotTrack?: boolean;
18
+ autoResetOnWindowOpen?: boolean;
17
19
  __DISABLE_SECURE_MODE?: boolean;
18
20
  };
19
21
  export default class API {
@@ -23,7 +25,7 @@ export default class API {
23
25
  use<T>(fn: (app: App | null, options?: Options) => T): T;
24
26
  isActive(): boolean;
25
27
  active(): boolean;
26
- start(): Promise<import("./app/index.js").OnStartInfo>;
28
+ start(startOpts?: StartOptions): Promise<OnStartInfo>;
27
29
  stop(): void;
28
30
  getSessionToken(): string | null | undefined;
29
31
  getSessionID(): string | null | undefined;
package/lib/index.js CHANGED
@@ -77,7 +77,7 @@ export default class API {
77
77
  (navigator.doNotTrack == '1'
78
78
  // @ts-ignore
79
79
  || window.doNotTrack == '1');
80
- this.app = doNotTrack ||
80
+ const app = this.app = doNotTrack ||
81
81
  !('Map' in window) ||
82
82
  !('Set' in window) ||
83
83
  !('MutationObserver' in window) ||
@@ -88,20 +88,34 @@ export default class API {
88
88
  !('Worker' in window)
89
89
  ? null
90
90
  : new App(options.projectKey, options.sessionToken, options);
91
- if (this.app !== null) {
92
- Viewport(this.app);
93
- CSSRules(this.app);
94
- Connection(this.app);
95
- Console(this.app, options);
96
- Exception(this.app, options);
97
- Img(this.app);
98
- Input(this.app, options);
99
- Mouse(this.app);
100
- Timing(this.app, options);
101
- Performance(this.app, options);
102
- Scroll(this.app);
103
- Longtasks(this.app);
91
+ if (app !== null) {
92
+ Viewport(app);
93
+ CSSRules(app);
94
+ Connection(app);
95
+ Console(app, options);
96
+ Exception(app, options);
97
+ Img(app);
98
+ Input(app, options);
99
+ Mouse(app);
100
+ Timing(app, options);
101
+ Performance(app, options);
102
+ Scroll(app);
103
+ Longtasks(app);
104
104
  window.__OPENREPLAY__ = this;
105
+ if (options.autoResetOnWindowOpen) {
106
+ const wOpen = window.open;
107
+ app.attachStartCallback(() => {
108
+ // @ts-ignore ?
109
+ window.open = function (...args) {
110
+ app.resetNextPageSession(true);
111
+ wOpen.call(window, ...args);
112
+ app.resetNextPageSession(false);
113
+ };
114
+ });
115
+ app.attachStopCallback(() => {
116
+ window.open = wOpen;
117
+ });
118
+ }
105
119
  }
106
120
  else {
107
121
  console.log("OpenReplay: browser doesn't support API required for tracking or doNotTrack is set to 1.");
@@ -111,7 +125,7 @@ export default class API {
111
125
  // no-cors issue only with text/plain or not-set Content-Type
112
126
  // req.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
113
127
  req.send(JSON.stringify({
114
- trackerVersion: '3.4.15',
128
+ trackerVersion: '3.4.17',
115
129
  projectKey: options.projectKey,
116
130
  doNotTrack,
117
131
  // TODO: add precise reason (an exact API missing)
@@ -131,7 +145,7 @@ export default class API {
131
145
  deprecationWarn("'active' method", "'isActive' method", "/");
132
146
  return this.isActive();
133
147
  }
134
- start() {
148
+ start(startOpts) {
135
149
  if (!IN_BROWSER) {
136
150
  console.error(`OpenReplay: you are trying to start Tracker on a node.js environment. If you want to use OpenReplay with SSR, please, use componentDidMount or useEffect API for placing the \`tracker.start()\` line. Check documentation on ${DOCS_HOST}${DOCS_SETUP}`);
137
151
  return Promise.reject("Trying to start not in browser.");
@@ -139,7 +153,7 @@ export default class API {
139
153
  if (this.app === null) {
140
154
  return Promise.reject("Browser doesn't support required api, or doNotTrack is active.");
141
155
  }
142
- return this.app.start();
156
+ return this.app.start(startOpts);
143
157
  }
144
158
  stop() {
145
159
  if (this.app === null) {
@@ -37,7 +37,14 @@ export function getExceptionMessageFromEvent(e) {
37
37
  return getExceptionMessage(e.reason, []);
38
38
  }
39
39
  else {
40
- return new JSException('Unhandled Promise Rejection', String(e.reason), '[]');
40
+ let message;
41
+ try {
42
+ message = JSON.stringify(e.reason);
43
+ }
44
+ catch (_) {
45
+ message = String(e.reason);
46
+ }
47
+ return new JSException('Unhandled Promise Rejection', message, '[]');
41
48
  }
42
49
  }
43
50
  return null;
@@ -1,6 +1,17 @@
1
1
  import { timestamp, isURL } from "../utils.js";
2
- import { ResourceTiming, SetNodeAttributeURLBased } from "../messages/index.js";
2
+ import { ResourceTiming, SetNodeAttributeURLBased, SetNodeAttribute } from "../messages/index.js";
3
+ const PLACEHOLDER_SRC = "https://static.openreplay.com/tracker/placeholder.jpeg";
3
4
  export default function (app) {
5
+ function sendPlaceholder(id, node) {
6
+ app.send(new SetNodeAttribute(id, "src", PLACEHOLDER_SRC));
7
+ const { width, height } = node.getBoundingClientRect();
8
+ if (!node.hasAttribute("width")) {
9
+ app.send(new SetNodeAttribute(id, "width", String(width)));
10
+ }
11
+ if (!node.hasAttribute("height")) {
12
+ app.send(new SetNodeAttribute(id, "height", String(height)));
13
+ }
14
+ }
4
15
  const sendImgSrc = app.safe(function () {
5
16
  const id = app.nodes.getID(this);
6
17
  if (id === undefined) {
@@ -15,7 +26,10 @@ export default function (app) {
15
26
  app.send(new ResourceTiming(timestamp(), 0, 0, 0, 0, 0, src, 'img'));
16
27
  }
17
28
  }
18
- else if (src.length < 1e5) {
29
+ else if (src.length >= 1e5 || app.sanitizer.isMasked(id)) {
30
+ sendPlaceholder(id, this);
31
+ }
32
+ else {
19
33
  app.send(new SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref()));
20
34
  }
21
35
  });
@@ -1,5 +1,6 @@
1
1
  import App from "../app/index.js";
2
- export declare function getInputLabel(node: HTMLInputElement): string;
2
+ declare type TextEditableElement = HTMLInputElement | HTMLTextAreaElement;
3
+ export declare function getInputLabel(node: TextEditableElement): string;
3
4
  export declare const enum InputMode {
4
5
  Plain = 0,
5
6
  Obscured = 1,
@@ -11,3 +12,4 @@ export interface Options {
11
12
  defaultInputMode: InputMode;
12
13
  }
13
14
  export default function (app: App, opts: Partial<Options>): void;
15
+ export {};
@@ -1,6 +1,9 @@
1
1
  import { normSpaces, IN_BROWSER, getLabelAttribute, hasOpenreplayAttribute } from "../utils.js";
2
2
  import { SetInputTarget, SetInputValue, SetInputChecked } from "../messages/index.js";
3
- function isInput(node) {
3
+ function isTextEditable(node) {
4
+ if (node instanceof HTMLTextAreaElement) {
5
+ return true;
6
+ }
4
7
  if (!(node instanceof HTMLInputElement)) {
5
8
  return false;
6
9
  }
@@ -107,7 +110,7 @@ export default function (app, opts) {
107
110
  app.ticker.attach(() => {
108
111
  inputValues.forEach((value, id) => {
109
112
  const node = app.nodes.getNode(id);
110
- if (!isInput(node)) {
113
+ if (!isTextEditable(node)) {
111
114
  inputValues.delete(id);
112
115
  return;
113
116
  }
@@ -138,7 +141,7 @@ export default function (app, opts) {
138
141
  if (id === undefined) {
139
142
  return;
140
143
  }
141
- if (isInput(node)) {
144
+ if (isTextEditable(node)) {
142
145
  inputValues.set(id, node.value);
143
146
  sendInputValue(id, node);
144
147
  return;
@@ -83,7 +83,7 @@ export default function (app) {
83
83
  tag === 'LI' ||
84
84
  target.onclick != null ||
85
85
  target.getAttribute('role') === 'button') {
86
- const label = app.observer.getInnerTextSecure(target);
86
+ const label = app.sanitizer.getInnerTextSecure(target);
87
87
  return normSpaces(label).slice(0, 100);
88
88
  }
89
89
  return '';
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": "3.4.15",
4
+ "version": "3.4.17",
5
5
  "keywords": [
6
6
  "logging",
7
7
  "replay"
@@ -1,47 +0,0 @@
1
- import App from "./index.js";
2
- interface Window extends WindowProxy {
3
- HTMLInputElement: typeof HTMLInputElement;
4
- HTMLLinkElement: typeof HTMLLinkElement;
5
- HTMLStyleElement: typeof HTMLStyleElement;
6
- SVGStyleElement: typeof SVGStyleElement;
7
- HTMLIFrameElement: typeof HTMLIFrameElement;
8
- Text: typeof Text;
9
- Element: typeof Element;
10
- }
11
- export interface Options {
12
- obscureTextEmails: boolean;
13
- obscureTextNumbers: boolean;
14
- captureIFrames: boolean;
15
- }
16
- export default class Observer {
17
- private readonly app;
18
- private readonly options;
19
- private readonly context;
20
- private readonly observer;
21
- private readonly commited;
22
- private readonly recents;
23
- private readonly indexes;
24
- private readonly attributesList;
25
- private readonly textSet;
26
- private readonly textMasked;
27
- constructor(app: App, options: Options, context?: Window);
28
- private clear;
29
- private isInstance;
30
- private isIgnored;
31
- private sendNodeAttribute;
32
- getInnerTextSecure(el: HTMLElement): string;
33
- private checkObscure;
34
- private sendNodeData;
35
- private bindNode;
36
- private bindTree;
37
- private unbindNode;
38
- private _commitNode;
39
- private commitNode;
40
- private commitNodes;
41
- private iframeObservers;
42
- private handleIframe;
43
- private observeIframe;
44
- observe(): void;
45
- disconnect(): void;
46
- }
47
- export {};