@openreplay/tracker 3.4.16 → 3.4.17-beta.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.
@@ -2,7 +2,18 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const utils_js_1 = require("../utils.js");
4
4
  const index_js_1 = require("../messages/index.js");
5
+ const PLACEHOLDER_SRC = "https://static.openreplay.com/tracker/placeholder.jpeg";
5
6
  function default_1(app) {
7
+ function sendPlaceholder(id, node) {
8
+ app.send(new index_js_1.SetNodeAttribute(id, "src", PLACEHOLDER_SRC));
9
+ const { width, height } = node.getBoundingClientRect();
10
+ if (!node.hasAttribute("width")) {
11
+ app.send(new index_js_1.SetNodeAttribute(id, "width", String(width)));
12
+ }
13
+ if (!node.hasAttribute("height")) {
14
+ app.send(new index_js_1.SetNodeAttribute(id, "height", String(height)));
15
+ }
16
+ }
6
17
  const sendImgSrc = app.safe(function () {
7
18
  const id = app.nodes.getID(this);
8
19
  if (id === undefined) {
@@ -17,7 +28,10 @@ function default_1(app) {
17
28
  app.send(new index_js_1.ResourceTiming((0, utils_js_1.timestamp)(), 0, 0, 0, 0, 0, src, 'img'));
18
29
  }
19
30
  }
20
- else if (src.length < 1e5) {
31
+ else if (src.length >= 1e5 || app.sanitizer.isMasked(id)) {
32
+ sendPlaceholder(id, this);
33
+ }
34
+ else {
21
35
  app.send(new index_js_1.SetNodeAttributeURLBased(id, 'src', src, app.getBaseHref()));
22
36
  }
23
37
  });
@@ -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 {};
@@ -3,7 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getInputLabel = void 0;
4
4
  const utils_js_1 = require("../utils.js");
5
5
  const index_js_1 = require("../messages/index.js");
6
- function isInput(node) {
6
+ function isTextEditable(node) {
7
+ if (node instanceof HTMLTextAreaElement) {
8
+ return true;
9
+ }
7
10
  if (!(node instanceof HTMLInputElement)) {
8
11
  return false;
9
12
  }
@@ -111,7 +114,7 @@ function default_1(app, opts) {
111
114
  app.ticker.attach(() => {
112
115
  inputValues.forEach((value, id) => {
113
116
  const node = app.nodes.getNode(id);
114
- if (!isInput(node)) {
117
+ if (!isTextEditable(node)) {
115
118
  inputValues.delete(id);
116
119
  return;
117
120
  }
@@ -142,7 +145,7 @@ function default_1(app, opts) {
142
145
  if (id === undefined) {
143
146
  return;
144
147
  }
145
- if (isInput(node)) {
148
+ if (isTextEditable(node)) {
146
149
  inputValues.set(id, node.value);
147
150
  sendInputValue(id, node);
148
151
  return;
@@ -85,7 +85,7 @@ function default_1(app) {
85
85
  tag === 'LI' ||
86
86
  target.onclick != null ||
87
87
  target.getAttribute('role') === 'button') {
88
- const label = app.observer.getInnerTextSecure(target);
88
+ const label = app.sanitizer.getInnerTextSecure(target);
89
89
  return (0, utils_js_1.normSpaces)(label).slice(0, 100);
90
90
  }
91
91
  return '';
@@ -0,0 +1,18 @@
1
+ export interface Window extends globalThis.Window {
2
+ HTMLInputElement: typeof HTMLInputElement;
3
+ HTMLLinkElement: typeof HTMLLinkElement;
4
+ HTMLStyleElement: typeof HTMLStyleElement;
5
+ SVGStyleElement: typeof SVGStyleElement;
6
+ HTMLIFrameElement: typeof HTMLIFrameElement;
7
+ Text: typeof Text;
8
+ Element: typeof Element;
9
+ ShadowRoot: typeof ShadowRoot;
10
+ }
11
+ declare type WindowConstructor = Document | Element | Text | ShadowRoot | HTMLInputElement | HTMLLinkElement | HTMLStyleElement | HTMLIFrameElement;
12
+ declare type Constructor<T> = {
13
+ new (...args: any[]): T;
14
+ name: string;
15
+ };
16
+ export declare function isInstance<T extends WindowConstructor>(node: Node, constr: Constructor<T>): node is T;
17
+ export declare function inDocument(node: Node): boolean;
18
+ export {};
@@ -0,0 +1,43 @@
1
+ // TODO: we need a type expert here so we won't have to ignore the lines
2
+ // TODO: use it everywhere (static function; export from which file? <-- global Window typing required)
3
+ export function isInstance(node, constr) {
4
+ const doc = node.ownerDocument;
5
+ if (!doc) { // null if Document
6
+ return constr.name === 'Document';
7
+ }
8
+ let context =
9
+ // @ts-ignore (for EI, Safary)
10
+ doc.parentWindow ||
11
+ doc.defaultView; // TODO: smart global typing for Window object
12
+ while (context.parent && context.parent !== context) {
13
+ // @ts-ignore
14
+ if (node instanceof context[constr.name]) {
15
+ return true;
16
+ }
17
+ // @ts-ignore
18
+ context = context.parent;
19
+ }
20
+ // @ts-ignore
21
+ return node instanceof context[constr.name];
22
+ }
23
+ export function inDocument(node) {
24
+ const doc = node.ownerDocument;
25
+ if (!doc) {
26
+ return false;
27
+ }
28
+ if (doc.contains(node)) {
29
+ return true;
30
+ }
31
+ let context =
32
+ // @ts-ignore (for EI, Safary)
33
+ doc.parentWindow ||
34
+ doc.defaultView;
35
+ while (context.parent && context.parent !== context) {
36
+ if (context.document.contains(node)) {
37
+ return true;
38
+ }
39
+ // @ts-ignore
40
+ context = context.parent;
41
+ }
42
+ return false;
43
+ }
@@ -1,19 +1,21 @@
1
1
  import Message from "../messages/message.js";
2
2
  import Nodes from "./nodes.js";
3
- import Observer from "./observer/top_observer.js";
3
+ import Sanitizer from "./sanitizer.js";
4
4
  import Ticker from "./ticker.js";
5
5
  import type { Options as ObserverOptions } from "./observer/top_observer.js";
6
+ import type { Options as SanitizerOptions } from "./sanitizer.js";
6
7
  import type { Options as WebworkerOptions } from "../messages/webworker.js";
7
8
  export interface OnStartInfo {
8
9
  sessionID: string;
9
10
  sessionToken: string;
10
11
  userUUID: string;
11
12
  }
12
- export declare type Options = {
13
+ declare type AppOptions = {
13
14
  revID: string;
14
15
  node_id: string;
15
16
  session_token_key: string;
16
17
  session_pageno_key: string;
18
+ session_reset_key: string;
17
19
  local_uuid_key: string;
18
20
  ingestPoint: string;
19
21
  resourceBaseHref: string | null;
@@ -21,7 +23,8 @@ export declare type Options = {
21
23
  __debug_report_edp: string | null;
22
24
  __debug_log: boolean;
23
25
  onStart?: (info: OnStartInfo) => void;
24
- } & ObserverOptions & WebworkerOptions;
26
+ } & WebworkerOptions;
27
+ export declare type Options = AppOptions & ObserverOptions & SanitizerOptions;
25
28
  declare type Callback = () => void;
26
29
  declare type CommitCallback = (messages: Array<Message>) => void;
27
30
  export declare const DEFAULT_INGEST_POINT = "https://api.openreplay.com/ingest";
@@ -29,8 +32,9 @@ export default class App {
29
32
  readonly nodes: Nodes;
30
33
  readonly ticker: Ticker;
31
34
  readonly projectKey: string;
35
+ readonly sanitizer: Sanitizer;
32
36
  private readonly messages;
33
- readonly observer: Observer;
37
+ private readonly observer;
34
38
  private readonly startCallbacks;
35
39
  private readonly stopCallbacks;
36
40
  private readonly commitCallbacks;
@@ -40,7 +44,7 @@ export default class App {
40
44
  private isActive;
41
45
  private version;
42
46
  private readonly worker?;
43
- constructor(projectKey: string, sessionToken: string | null | undefined, opts: Partial<Options>);
47
+ constructor(projectKey: string, sessionToken: string | null | undefined, options: Partial<Options>);
44
48
  private _debug;
45
49
  send(message: Message, urgent?: boolean): void;
46
50
  private commit;
@@ -58,6 +62,7 @@ export default class App {
58
62
  resolveResourceURL(resourceURL: string): string;
59
63
  isServiceURL(url: string): boolean;
60
64
  active(): boolean;
65
+ resetNextPageSession(flag: boolean): void;
61
66
  private _start;
62
67
  start(reset?: boolean): Promise<OnStartInfo>;
63
68
  stop(): void;
package/lib/app/index.js CHANGED
@@ -2,41 +2,41 @@ import { timestamp, log, warn } from "../utils.js";
2
2
  import { Timestamp } from "../messages/index.js";
3
3
  import Nodes from "./nodes.js";
4
4
  import Observer from "./observer/top_observer.js";
5
+ import Sanitizer from "./sanitizer.js";
5
6
  import Ticker from "./ticker.js";
6
7
  import { deviceMemory, jsHeapSizeLimit } from "../modules/performance.js";
7
8
  // TODO: use backendHost only
8
9
  export const DEFAULT_INGEST_POINT = 'https://api.openreplay.com/ingest';
9
10
  export default class App {
10
- constructor(projectKey, sessionToken, opts) {
11
+ constructor(projectKey, sessionToken, options) {
11
12
  this.messages = [];
12
13
  this.startCallbacks = [];
13
14
  this.stopCallbacks = [];
14
15
  this.commitCallbacks = [];
15
16
  this._sessionID = null;
16
17
  this.isActive = false;
17
- this.version = '3.4.16';
18
+ this.version = '3.4.17-beta.0';
18
19
  this.projectKey = projectKey;
19
20
  this.options = Object.assign({
20
21
  revID: '',
21
22
  node_id: '__openreplay_id',
22
23
  session_token_key: '__openreplay_token',
23
24
  session_pageno_key: '__openreplay_pageno',
25
+ session_reset_key: '__openreplay_reset',
24
26
  local_uuid_key: '__openreplay_uuid',
25
27
  ingestPoint: DEFAULT_INGEST_POINT,
26
28
  resourceBaseHref: null,
27
29
  __is_snippet: false,
28
30
  __debug_report_edp: null,
29
31
  __debug_log: false,
30
- obscureTextEmails: true,
31
- obscureTextNumbers: false,
32
- captureIFrames: false,
33
- }, opts);
32
+ }, options);
34
33
  if (sessionToken != null) {
35
34
  sessionStorage.setItem(this.options.session_token_key, sessionToken);
36
35
  }
37
36
  this.revID = this.options.revID;
37
+ this.sanitizer = new Sanitizer(this, options);
38
38
  this.nodes = new Nodes(this.options.node_id);
39
- this.observer = new Observer(this, this.options);
39
+ this.observer = new Observer(this, options);
40
40
  this.ticker = new Ticker(this);
41
41
  this.ticker.attach(() => this.commit());
42
42
  try {
@@ -178,6 +178,14 @@ export default class App {
178
178
  active() {
179
179
  return this.isActive;
180
180
  }
181
+ resetNextPageSession(flag) {
182
+ if (flag) {
183
+ sessionStorage.setItem(this.options.session_reset_key, 't');
184
+ }
185
+ else {
186
+ sessionStorage.removeItem(this.options.session_reset_key);
187
+ }
188
+ }
181
189
  _start(reset) {
182
190
  if (!this.isActive) {
183
191
  if (!this.worker) {
@@ -205,6 +213,8 @@ export default class App {
205
213
  // if (tokenIsActive) {
206
214
  // token = null
207
215
  // }
216
+ const sReset = sessionStorage.getItem(this.options.session_reset_key);
217
+ sessionStorage.removeItem(this.options.session_reset_key);
208
218
  return window.fetch(this.options.ingestPoint + '/v1/web/start', {
209
219
  method: 'POST',
210
220
  headers: {
@@ -220,7 +230,7 @@ export default class App {
220
230
  isSnippet: this.options.__is_snippet,
221
231
  deviceMemory,
222
232
  jsHeapSizeLimit,
223
- reset,
233
+ reset: reset || sReset !== null,
224
234
  }),
225
235
  })
226
236
  .then(r => {
@@ -298,6 +308,7 @@ export default class App {
298
308
  if (this.worker) {
299
309
  this.worker.postMessage("stop");
300
310
  }
311
+ this.sanitizer.clear();
301
312
  this.observer.disconnect();
302
313
  this.nodes.clear();
303
314
  this.ticker.stop();
@@ -1,25 +1,5 @@
1
1
  import App from "../index.js";
2
- export interface Window extends globalThis.Window {
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
- ShadowRoot: typeof ShadowRoot;
11
- }
12
- declare type WindowConstructor = Document | Element | Text | ShadowRoot | HTMLInputElement | HTMLLinkElement | HTMLStyleElement | HTMLIFrameElement;
13
- declare type Constructor<T> = {
14
- new (...args: any[]): T;
15
- name: string;
16
- };
17
- export declare function isInstance<T extends WindowConstructor>(node: Node, constr: Constructor<T>): node is T;
18
- export interface Options {
19
- obscureTextEmails: boolean;
20
- obscureTextNumbers: boolean;
21
- }
22
- export default abstract class Observer<AdditionalOptions = {}> {
2
+ export default abstract class Observer {
23
3
  protected readonly app: App;
24
4
  protected readonly context: Window;
25
5
  private readonly observer;
@@ -29,14 +9,10 @@ export default abstract class Observer<AdditionalOptions = {}> {
29
9
  private readonly indexes;
30
10
  private readonly attributesList;
31
11
  private readonly textSet;
32
- private readonly textMasked;
33
- protected readonly options: Options & AdditionalOptions;
34
12
  private readonly inUpperContext;
35
- constructor(app: App, options: Partial<Options> & AdditionalOptions, context?: Window);
13
+ constructor(app: App, context?: Window);
36
14
  private clear;
37
15
  private sendNodeAttribute;
38
- getInnerTextSecure(el: HTMLElement): string;
39
- private checkObscure;
40
16
  private sendNodeData;
41
17
  private bindNode;
42
18
  private bindTree;
@@ -47,4 +23,3 @@ export default abstract class Observer<AdditionalOptions = {}> {
47
23
  protected observeRoot(node: Node, beforeCommit: (id?: number) => unknown, nodeToBind?: Node): void;
48
24
  disconnect(): void;
49
25
  }
50
- export {};
@@ -1,30 +1,8 @@
1
- import { stars, hasOpenreplayAttribute } from "../../utils.js";
2
1
  import { RemoveNodeAttribute, SetNodeAttribute, SetNodeAttributeURLBased, SetCSSDataURLBased, SetNodeData, CreateTextNode, CreateElementNode, MoveNode, RemoveNode, } from "../../messages/index.js";
2
+ import { isInstance, inDocument } from "../context.js";
3
3
  function isSVGElement(node) {
4
4
  return node.namespaceURI === 'http://www.w3.org/2000/svg';
5
5
  }
6
- // TODO: we need a type expert here so we won't have to ignore the lines
7
- // TODO: use it everywhere (static function; export from which file? <-- global Window typing required)
8
- export function isInstance(node, constr) {
9
- const doc = node.ownerDocument;
10
- if (!doc) { // null if Document
11
- return constr.name === 'Document';
12
- }
13
- let context =
14
- // @ts-ignore (for EI, Safary)
15
- doc.parentWindow ||
16
- doc.defaultView; // TODO: smart global typing for Window object
17
- while (context.parent && context.parent !== context) {
18
- // @ts-ignore
19
- if (node instanceof context[constr.name]) {
20
- return true;
21
- }
22
- // @ts-ignore
23
- context = context.parent;
24
- }
25
- // @ts-ignore
26
- return node instanceof context[constr.name];
27
- }
28
6
  function isIgnored(node) {
29
7
  if (isInstance(node, Text)) {
30
8
  return false;
@@ -54,7 +32,7 @@ function isObservable(node) {
54
32
  return !isIgnored(node);
55
33
  }
56
34
  export default class Observer {
57
- constructor(app, options, context = window) {
35
+ constructor(app, context = window) {
58
36
  this.app = app;
59
37
  this.context = context;
60
38
  this.commited = [];
@@ -63,34 +41,12 @@ export default class Observer {
63
41
  this.indexes = [];
64
42
  this.attributesList = [];
65
43
  this.textSet = new Set();
66
- this.textMasked = new Set();
67
- this.options = Object.assign({
68
- obscureTextEmails: true,
69
- obscureTextNumbers: false,
70
- }, options);
71
- this.inUpperContext = context.parent === context;
44
+ this.inUpperContext = context.parent === context; //TODO: get rid of context here
72
45
  this.observer = new MutationObserver(this.app.safe((mutations) => {
73
46
  for (const mutation of mutations) {
74
47
  const target = mutation.target;
75
48
  const type = mutation.type;
76
- // TODO TODO TODO: move to iframe_observer/remove??? (check if )
77
- // Special case
78
- // 'childList' on Document might happen in case of iframe.
79
- // TODO: generalize as much as possible
80
- // if (isInstance(target, Document) // Also ShadowRoot can be here
81
- // && type === 'childList'
82
- // //&& new Array(mutation.addedNodes).some(node => isInstance(node, HTMLHtmlElement))
83
- // ) {
84
- // const parentFrame = target.defaultView?.frameElement
85
- // if (!parentFrame) { continue }
86
- // this.bindTree(target.documentElement)
87
- // const frameID = this.app.nodes.getID(parentFrame)
88
- // const docID = this.app.nodes.getID(target.documentElement)
89
- // if (frameID === undefined || docID === undefined) { continue }
90
- // this.app.send(CreateIFrameDocument(frameID, docID));
91
- // continue;
92
- // }
93
- if (!isObservable(target) || !context.document.contains(target)) {
49
+ if (!isObservable(target) || !inDocument(target)) {
94
50
  continue;
95
51
  }
96
52
  if (type === 'childList') {
@@ -135,7 +91,6 @@ export default class Observer {
135
91
  this.indexes.length = 1;
136
92
  this.attributesList.length = 0;
137
93
  this.textSet.clear();
138
- //this.textMasked.clear();
139
94
  }
140
95
  sendNodeAttribute(id, node, name, value) {
141
96
  if (isSVGElement(node)) {
@@ -184,35 +139,14 @@ export default class Observer {
184
139
  }
185
140
  this.app.send(new SetNodeAttribute(id, name, value));
186
141
  }
187
- /* TODO: abstract sanitation */
188
- getInnerTextSecure(el) {
189
- const id = this.app.nodes.getID(el);
190
- if (!id) {
191
- return '';
192
- }
193
- return this.checkObscure(id, el.innerText);
194
- }
195
- checkObscure(id, data) {
196
- if (this.textMasked.has(id)) {
197
- return data.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█');
198
- }
199
- if (this.options.obscureTextNumbers) {
200
- data = data.replace(/\d/g, '0');
201
- }
202
- if (this.options.obscureTextEmails) {
203
- data = data.replace(/([^\s]+)@([^\s]+)\.([^\s]+)/g, (...f) => stars(f[1]) + '@' + stars(f[2]) + '.' + stars(f[3]));
204
- }
205
- return data;
206
- }
207
142
  sendNodeData(id, parentElement, data) {
208
143
  if (isInstance(parentElement, HTMLStyleElement) || isInstance(parentElement, SVGStyleElement)) {
209
144
  this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref()));
210
145
  return;
211
146
  }
212
- data = this.checkObscure(id, data);
147
+ data = this.app.sanitizer.sanitize(id, data);
213
148
  this.app.send(new SetNodeData(id, data));
214
149
  }
215
- /* end TODO: abstract sanitation */
216
150
  bindNode(node) {
217
151
  const r = this.app.nodes.registerNode(node);
218
152
  const id = r[0];
@@ -264,10 +198,7 @@ export default class Observer {
264
198
  this.unbindNode(node);
265
199
  return false;
266
200
  }
267
- if (this.textMasked.has(parentID) ||
268
- (isInstance(node, Element) && hasOpenreplayAttribute(node, 'masked'))) {
269
- this.textMasked.add(id);
270
- }
201
+ this.app.sanitizer.handleNode(id, parentID, node);
271
202
  }
272
203
  let sibling = node.previousSibling;
273
204
  while (sibling !== null) {
@@ -340,8 +271,7 @@ export default class Observer {
340
271
  let node;
341
272
  for (let id = 0; id < this.recents.length; id++) {
342
273
  // TODO: make things/logic nice here.
343
- // commit required in any case if recents[id] true or false (in case of unbinding).
344
- // ???!?!?R@TW:$HKJ$WLKn
274
+ // commit required in any case if recents[id] true or false (in case of unbinding) or undefined (in case of attr change).
345
275
  if (!this.myNodes[id]) {
346
276
  continue;
347
277
  }
@@ -369,8 +299,6 @@ export default class Observer {
369
299
  disconnect() {
370
300
  this.observer.disconnect();
371
301
  this.clear();
372
- // to sanitizer
373
- this.textMasked.clear();
374
302
  this.myNodes.length = 0;
375
303
  }
376
304
  }
@@ -1,10 +1,10 @@
1
1
  import Observer from "./observer.js";
2
- import type { Options as BaseOptions } from "./observer.js";
3
2
  import App from "../index.js";
4
- export interface Options extends Partial<BaseOptions> {
3
+ export interface Options {
5
4
  captureIFrames: boolean;
6
5
  }
7
- export default class TopObserver extends Observer<Options> {
6
+ export default class TopObserver extends Observer {
7
+ private readonly options;
8
8
  constructor(app: App, options: Partial<Options>);
9
9
  private iframeObservers;
10
10
  private handleIframe;
@@ -1,15 +1,17 @@
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
6
  const attachShadowNativeFn = Element.prototype.attachShadow;
6
7
  export default class TopObserver extends Observer {
7
8
  constructor(app, options) {
8
- super(app, Object.assign({
9
- captureIFrames: false
10
- }, options));
9
+ super(app);
11
10
  this.iframeObservers = [];
12
11
  this.shadowRootObservers = [];
12
+ this.options = Object.assign({
13
+ captureIFrames: false
14
+ }, options);
13
15
  // IFrames
14
16
  this.app.nodes.attachNodeCallback(node => {
15
17
  if (isInstance(node, HTMLIFrameElement) &&
@@ -38,7 +40,7 @@ export default class TopObserver extends Observer {
38
40
  if (!context) {
39
41
  return;
40
42
  }
41
- const observer = new IFrameObserver(this.app, this.options, context);
43
+ const observer = new IFrameObserver(this.app, context);
42
44
  this.iframeObservers.push(observer);
43
45
  observer.observe(iframe);
44
46
  });
@@ -46,7 +48,7 @@ export default class TopObserver extends Observer {
46
48
  handle();
47
49
  }
48
50
  handleShadowRoot(shRoot) {
49
- const observer = new ShadowRootObserver(this.app, this.options, this.context);
51
+ const observer = new ShadowRootObserver(this.app, this.context);
50
52
  this.shadowRootObservers.push(observer);
51
53
  observer.observe(shRoot.host);
52
54
  }
@@ -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
@@ -37,4 +37,5 @@ export default class API {
37
37
  event(key: string, payload: any, issue?: boolean): void;
38
38
  issue(key: string, payload: any): void;
39
39
  handleError: (e: Error | ErrorEvent | PromiseRejectionEvent) => void;
40
+ resetNextPageSession(flag: boolean): void;
40
41
  }
package/lib/index.js CHANGED
@@ -111,7 +111,7 @@ export default class API {
111
111
  // no-cors issue only with text/plain or not-set Content-Type
112
112
  // req.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
113
113
  req.send(JSON.stringify({
114
- trackerVersion: '3.4.16',
114
+ trackerVersion: '3.4.17-beta.0',
115
115
  projectKey: options.projectKey,
116
116
  doNotTrack,
117
117
  // TODO: add precise reason (an exact API missing)
@@ -219,4 +219,10 @@ export default class API {
219
219
  this.app.send(new CustomIssue(key, payload));
220
220
  }
221
221
  }
222
+ resetNextPageSession(flag) {
223
+ if (typeof flag !== 'boolean' || !this.app) {
224
+ return;
225
+ }
226
+ this.app.resetNextPageSession(flag);
227
+ }
222
228
  }