clarity-js 0.6.33 → 0.6.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-js",
3
- "version": "0.6.33",
3
+ "version": "0.6.36",
4
4
  "description": "An analytics library that uses web page interactions to generate aggregated insights",
5
5
  "author": "Microsoft Corp.",
6
6
  "license": "MIT",
@@ -11,6 +11,7 @@ let config: Config = {
11
11
  regions: [],
12
12
  extract: [],
13
13
  cookies: [],
14
+ fraud: [],
14
15
  report: null,
15
16
  upload: null,
16
17
  fallback: null,
@@ -1,7 +1,9 @@
1
- import { Code, Constant, Setting, Severity } from "@clarity-types/data";
1
+ import { BooleanFlag, Code, Constant, Metric, Setting, Severity } from "@clarity-types/data";
2
2
  import * as clarity from "@src/clarity";
3
+ import * as core from "@src/core"
3
4
  import { bind } from "@src/core/event";
4
5
  import * as internal from "@src/diagnostic/internal";
6
+ import * as metric from "@src/data/metric";
5
7
 
6
8
  let pushState = null;
7
9
  let replaceState = null;
@@ -12,24 +14,29 @@ export function start(): void {
12
14
  url = getCurrentUrl();
13
15
  count = 0;
14
16
  bind(window, "popstate", compute);
15
-
17
+
16
18
  // Add a proxy to history.pushState function
17
- if (pushState === null) { pushState = history.pushState; }
18
- history.pushState = function(): void {
19
- if (check()) {
19
+ if (pushState === null) {
20
+ pushState = history.pushState;
21
+ history.pushState = function(): void {
20
22
  pushState.apply(this, arguments);
21
- compute();
22
- }
23
- };
23
+ if (core.active() && check()) {
24
+ compute();
25
+ }
26
+ };
27
+ }
24
28
 
25
29
  // Add a proxy to history.replaceState function
26
- if (replaceState === null) { replaceState = history.replaceState; }
27
- history.replaceState = function(): void {
28
- if (check()) {
30
+ if (replaceState === null)
31
+ {
32
+ replaceState = history.replaceState;
33
+ history.replaceState = function(): void {
29
34
  replaceState.apply(this, arguments);
30
- compute();
31
- }
32
- };
35
+ if (core.active() && check()) {
36
+ compute();
37
+ }
38
+ };
39
+ }
33
40
  }
34
41
 
35
42
  function check(): boolean {
@@ -45,27 +52,20 @@ function compute(): void {
45
52
  if (url !== getCurrentUrl()) {
46
53
  // If the url changed, start tracking it as a new page
47
54
  clarity.stop();
48
- window.setTimeout(clarity.start, Setting.RestartDelay);
55
+ window.setTimeout(restart, Setting.RestartDelay);
49
56
  }
50
57
  }
51
58
 
59
+ function restart(): void {
60
+ clarity.start();
61
+ metric.max(Metric.SinglePage, BooleanFlag.True);
62
+ }
63
+
52
64
  function getCurrentUrl(): string {
53
65
  return location.href ? location.href.replace(location.hash, Constant.Empty) : location.href;
54
66
  }
55
67
 
56
68
  export function stop(): void {
57
- // Restore original function definition of history.pushState
58
- if (pushState !== null) {
59
- history.pushState = pushState;
60
- pushState = null;
61
- }
62
-
63
- // Restore original function definition of history.replaceState
64
- if (replaceState !== null) {
65
- history.replaceState = replaceState;
66
- replaceState = null;
67
- }
68
-
69
69
  url = null;
70
70
  count = 0;
71
71
  }
@@ -1,7 +1,7 @@
1
1
  import { Event } from "@clarity-types/data";
2
2
  import measure from "./measure";
3
3
 
4
- export function setTimeout(handler: (event?: Event | boolean) => void, timeout: number, event?: Event): number {
4
+ export function setTimeout(handler: (event?: Event | boolean) => void, timeout?: number, event?: Event): number {
5
5
  return window.setTimeout(measure(handler), timeout, event);
6
6
  }
7
7
 
@@ -1,2 +1,2 @@
1
- let version = "0.6.33";
1
+ let version = "0.6.36";
2
2
  export default version;
@@ -1,5 +1,5 @@
1
1
  import { Time } from "@clarity-types/core";
2
- import { BooleanFlag, Constant, Dimension, Metadata, MetadataCallback, Metric, Session, User, Setting } from "@clarity-types/data";
2
+ import { BooleanFlag, Constant, Dimension, Metadata, MetadataCallback, MetadataCallbackOptions, Metric, Session, User, Setting } from "@clarity-types/data";
3
3
  import * as core from "@src/core";
4
4
  import config from "@src/core/config";
5
5
  import hash from "@src/core/hash";
@@ -8,11 +8,10 @@ import * as metric from "@src/data/metric";
8
8
  import { set } from "@src/data/variable";
9
9
 
10
10
  export let data: Metadata = null;
11
- export let callback: MetadataCallback = null;
11
+ export let callbacks: MetadataCallbackOptions[] = [];
12
12
  let rootDomain = null;
13
13
 
14
14
  export function start(): void {
15
- callback = null;
16
15
  rootDomain = null;
17
16
  const ua = navigator && "userAgent" in navigator ? navigator.userAgent : Constant.Empty;
18
17
  const title = document && document.title ? document.title : Constant.Empty;
@@ -40,7 +39,6 @@ export function start(): void {
40
39
  dimension.log(Dimension.DocumentDirection, document.dir);
41
40
  if (navigator) {
42
41
  dimension.log(Dimension.Language, (<any>navigator).userLanguage || navigator.language);
43
- metric.max(Metric.Automation, navigator.webdriver ? BooleanFlag.True : BooleanFlag.False);
44
42
  userAgentData();
45
43
  }
46
44
 
@@ -64,7 +62,7 @@ export function start(): void {
64
62
  track(u);
65
63
  }
66
64
 
67
- export function userAgentData(): void {
65
+ function userAgentData(): void {
68
66
  if (navigator["userAgentData"] && navigator["userAgentData"].getHighEntropyValues) {
69
67
  navigator["userAgentData"].getHighEntropyValues(
70
68
  ["model",
@@ -84,7 +82,6 @@ export function userAgentData(): void {
84
82
  }
85
83
 
86
84
  export function stop(): void {
87
- callback = null;
88
85
  rootDomain = null;
89
86
  data = null;
90
87
  }
@@ -93,10 +90,9 @@ export function metadata(cb: MetadataCallback, wait: boolean = true): void {
93
90
  if (data && wait === false) {
94
91
  // Immediately invoke the callback if the caller explicitly doesn't want to wait for the upgrade confirmation
95
92
  cb(data, !config.lean);
96
- } else {
97
- // Save the callback for future reference; so we can inform the caller when page gets upgraded and we have a valid playback flag
98
- callback = cb;
99
93
  }
94
+
95
+ callbacks.push({callback: cb, wait: wait });
100
96
  }
101
97
 
102
98
  export function id(): string {
@@ -127,12 +123,20 @@ function tab(): string {
127
123
 
128
124
  export function save(): void {
129
125
  let ts = Math.round(Date.now());
130
- let upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
131
126
  let upload = config.upload && typeof config.upload === Constant.String ? (config.upload as string).replace(Constant.HTTPS, Constant.Empty) : Constant.Empty;
132
- if (upgrade && callback) { callback(data, !config.lean); }
127
+ let upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
128
+ processCallback(upgrade);
133
129
  setCookie(Constant.SessionKey, [data.sessionId, ts, data.pageNum, upgrade, upload].join(Constant.Pipe), Setting.SessionExpire);
134
130
  }
135
131
 
132
+ function processCallback(upgrade: BooleanFlag) {
133
+ if (callbacks.length > 0) {
134
+ callbacks.forEach(x => {
135
+ if (x.callback && (!x.wait || upgrade)) { x.callback(data, !config.lean); }
136
+ })
137
+ }
138
+ }
139
+
136
140
  function supported(target: Window | Document, api: string): boolean {
137
141
  try { return !!target[api]; } catch { return false; }
138
142
  }
@@ -15,11 +15,11 @@ export function stop(): void {
15
15
  updates = {};
16
16
  }
17
17
 
18
- export function count(metric: Metric, increment: number = 1): void {
18
+ export function count(metric: Metric): void {
19
19
  if (!(metric in data)) { data[metric] = 0; }
20
20
  if (!(metric in updates)) { updates[metric] = 0; }
21
- data[metric] += increment;
22
- updates[metric] += increment;
21
+ data[metric]++;
22
+ updates[metric]++;
23
23
  }
24
24
 
25
25
  export function sum(metric: Metric, value: number): void {
@@ -1,6 +1,7 @@
1
1
  import { Event, Token } from "@clarity-types/data";
2
2
  import { time } from "@src/core/time";
3
3
  import { queue } from "@src/data/upload";
4
+ import * as fraud from "@src/diagnostic/fraud";
4
5
  import * as internal from "@src/diagnostic/internal";
5
6
  import * as script from "@src/diagnostic/script";
6
7
 
@@ -26,5 +27,13 @@ export default async function (type: Event): Promise<void> {
26
27
  queue(tokens, false);
27
28
  }
28
29
  break;
30
+ case Event.Fraud:
31
+ if (fraud.data) {
32
+ tokens.push(fraud.data.id);
33
+ tokens.push(fraud.data.target);
34
+ tokens.push(fraud.data.hash);
35
+ queue(tokens, false);
36
+ }
37
+ break;
29
38
  }
30
39
  }
@@ -0,0 +1,29 @@
1
+ import { BooleanFlag, Event, Metric, Setting } from "@clarity-types/data";
2
+ import { FraudData } from "@clarity-types/diagnostic";
3
+ import hash from "@src/core/hash";
4
+ import * as metric from "@src/data/metric";
5
+ import encode from "./encode";
6
+
7
+ let history = [];
8
+ export let data: FraudData;
9
+
10
+ export function start(): void {
11
+ history = [];
12
+ metric.max(Metric.Automation, navigator.webdriver ? BooleanFlag.True : BooleanFlag.False);
13
+ }
14
+
15
+ export function check(id: number, target: number, input: string): void {
16
+ // Compute hash for fraud detection. Hash is computed only if input meets the minimum length criteria
17
+ if (id !== null && input && input.length >= Setting.WordLength) {
18
+ data = { id, target, hash: hash(input) };
19
+ // Only encode this event if we haven't already reported this hash
20
+ if (history.indexOf(data.hash) < 0) {
21
+ history.push(data.hash);
22
+ encode(Event.Fraud);
23
+ }
24
+ }
25
+ }
26
+
27
+ export function stop(): void {
28
+ history = [];
29
+ }
@@ -1,7 +1,9 @@
1
+ import * as fraud from "./fraud";
1
2
  import * as internal from "./internal";
2
3
  import * as script from "./script";
3
4
 
4
5
  export function start(): void {
6
+ fraud.start();
5
7
  script.start();
6
8
  internal.start();
7
9
  }
@@ -1,7 +1,6 @@
1
- import { Constant, Event, Setting } from "@clarity-types/data";
1
+ import { Event, Setting } from "@clarity-types/data";
2
2
  import { ScriptErrorData } from "@clarity-types/diagnostic";
3
3
  import { bind } from "@src/core/event";
4
- import * as box from "@src/layout/box";
5
4
  import encode from "./encode";
6
5
 
7
6
  let history: { [key: string]: number } = {};
@@ -29,15 +28,6 @@ function handler(error: ErrorEvent): boolean {
29
28
  source: error["filename"]
30
29
  };
31
30
 
32
- // In certain cases, ResizeObserver could lead to flood of benign errors - especially when video element is involved.
33
- // Reference Chromium issue: https://bugs.chromium.org/p/chromium/issues/detail?id=809574
34
- // Even though it doesn't impact user experience, or show up in console, it can still flood error reporting through on error
35
- // To mitigate that, we turn off Clarity's ResizeObserver on getting the first instance of this error
36
- if (e.message.indexOf(Constant.ResizeObserver) >= 0) {
37
- box.stop();
38
- return false;
39
- }
40
-
41
31
  encode(Event.ScriptError);
42
32
  }
43
33
 
@@ -65,7 +65,8 @@ function handler(event: Event, root: Node, evt: MouseEvent): void {
65
65
  context: context(a),
66
66
  text: text(t),
67
67
  link: a ? a.href : null,
68
- hash: null
68
+ hash: null,
69
+ trust: evt.isTrusted ? BooleanFlag.True : BooleanFlag.False
69
70
  }
70
71
  });
71
72
  schedule(encode.bind(this, event));
@@ -44,7 +44,7 @@ export default async function (type: Event): Promise<void> {
44
44
  break;
45
45
  case Event.Click:
46
46
  for (let entry of click.state) {
47
- let cTarget = metadata(entry.data.target as Node, entry.event);
47
+ let cTarget = metadata(entry.data.target as Node, entry.event, entry.data.text);
48
48
  tokens = [entry.time, entry.event];
49
49
  let cHash = cTarget.hash.join(Constant.Dot);
50
50
  tokens.push(cTarget.id);
@@ -58,6 +58,7 @@ export default async function (type: Event): Promise<void> {
58
58
  tokens.push(scrub(entry.data.text, "click", cTarget.privacy));
59
59
  tokens.push(entry.data.link);
60
60
  tokens.push(cHash);
61
+ tokens.push(entry.data.trust);
61
62
  queue(tokens);
62
63
  timeline.track(entry.time, entry.event, cHash, entry.data.x, entry.data.y, entry.data.reaction, entry.data.context);
63
64
  }
@@ -91,10 +92,10 @@ export default async function (type: Event): Promise<void> {
91
92
  break;
92
93
  case Event.Input:
93
94
  for (let entry of input.state) {
94
- let iTarget = metadata(entry.data.target as Node, entry.event);
95
+ let iTarget = metadata(entry.data.target as Node, entry.event, entry.data.value);
95
96
  tokens = [entry.time, entry.event];
96
97
  tokens.push(iTarget.id);
97
- tokens.push(entry.data.value);
98
+ tokens.push(scrub(entry.data.value, "input", iTarget.privacy));
98
99
  queue(tokens);
99
100
  }
100
101
  input.reset();
@@ -1,7 +1,6 @@
1
1
  import { Event } from "@clarity-types/data";
2
2
  import { InputData, InputState, Setting } from "@clarity-types/interaction";
3
3
  import { bind } from "@src/core/event";
4
- import scrub from "@src/core/scrub";
5
4
  import { schedule } from "@src/core/task";
6
5
  import { time } from "@src/core/time";
7
6
  import { clearTimeout, setTimeout } from "@src/core/timeout";
@@ -24,18 +23,12 @@ function recompute(evt: UIEvent): void {
24
23
  let input = target(evt) as HTMLInputElement;
25
24
  let value = get(input);
26
25
  if (input && input.type && value) {
27
- let v;
26
+ let v = input.value;
28
27
  switch (input.type) {
29
28
  case "radio":
30
29
  case "checkbox":
31
30
  v = input.checked ? "true" : "false";
32
31
  break;
33
- case "range":
34
- v = input.value;
35
- break;
36
- default:
37
- v = scrub(input.value, "input", value.metadata.privacy);
38
- break;
39
32
  }
40
33
 
41
34
  let data: InputData = { target: input, value: v };
package/src/layout/dom.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Privacy } from "@clarity-types/core";
2
- import { Code, Setting, Severity } from "@clarity-types/data";
3
- import { Constant, NodeInfo, NodeValue, Selector, SelectorInput, Source } from "@clarity-types/layout";
2
+ import { Code, Severity } from "@clarity-types/data";
3
+ import { Constant, Mask, NodeInfo, NodeMeta, NodeValue, Selector, SelectorInput, Source } from "@clarity-types/layout";
4
4
  import config from "@src/core/config";
5
5
  import hash from "@src/core/hash";
6
6
  import * as internal from "@src/diagnostic/internal";
@@ -9,12 +9,6 @@ import selector from "@src/layout/selector";
9
9
  import * as mutation from "@src/layout/mutation";
10
10
  import * as extract from "@src/data/extract";
11
11
  let index: number = 1;
12
-
13
- // Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#%3Cinput%3E_types
14
- const DISALLOWED_TYPES = ["password", "hidden", "email", "tel"];
15
- const DISALLOWED_NAMES = ["addr", "cell", "code", "dob", "email", "mob", "name", "phone", "secret", "social", "ssn", "tel", "zip", "pass", "card", "account", "cvv", "ccv"];
16
- const DISALLOWED_MATCH = ["address", "password", "contact"];
17
-
18
12
  let nodes: Node[] = [];
19
13
  let values: NodeValue[] = [];
20
14
  let updateMap: number[] = [];
@@ -22,11 +16,14 @@ let hashMap: { [hash: string]: number } = {};
22
16
  let override = [];
23
17
  let unmask = [];
24
18
  let updatedFragments: { [fragment: number]: string } = {};
19
+ let maskText = [];
20
+ let maskDisable = [];
25
21
 
26
22
  // The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced
27
23
  let idMap: WeakMap<Node, number> = null; // Maps node => id.
28
24
  let iframeMap: WeakMap<Document, HTMLIFrameElement> = null; // Maps iframe's contentDocument => parent iframe element
29
25
  let privacyMap: WeakMap<Node, Privacy> = null; // Maps node => Privacy (enum)
26
+ let fraudMap: WeakMap<Node, number> = null; // Maps node => FraudId (number)
30
27
 
31
28
  export function start(): void {
32
29
  reset();
@@ -45,9 +42,12 @@ function reset(): void {
45
42
  hashMap = {};
46
43
  override = [];
47
44
  unmask = [];
45
+ maskText = Mask.Text.split(Constant.Comma);
46
+ maskDisable = Mask.Disable.split(Constant.Comma);
48
47
  idMap = new WeakMap();
49
48
  iframeMap = new WeakMap();
50
49
  privacyMap = new WeakMap();
50
+ fraudMap = new WeakMap();
51
51
  }
52
52
 
53
53
  // We parse new root nodes for any regions or masked nodes in the beginning (document) and
@@ -64,6 +64,7 @@ export function parse(root: ParentNode, init: boolean = false): void {
64
64
  if ("querySelectorAll" in root) {
65
65
  config.regions.forEach(x => root.querySelectorAll(x[1]).forEach(e => region.observe(e, `${x[0]}`))); // Regions
66
66
  config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements
67
+ config.fraud.forEach(x => root.querySelectorAll(x[1]).forEach(e => fraudMap.set(e, x[0]))); // Fraud Check
67
68
  unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements
68
69
  }
69
70
  } catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
@@ -84,22 +85,20 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
84
85
  let id = getId(node, true);
85
86
  let parentId = parent ? getId(parent) : null;
86
87
  let previousId = getPreviousId(node);
87
- let privacy = config.content ? Privacy.Sensitive : Privacy.Text;
88
- let parentValue = null;
88
+ let parentValue: NodeValue = null;
89
89
  let regionId = region.exists(node) ? id : null;
90
90
  let fragmentId = null;
91
+ let fraudId = fraudMap.has(node) ? fraudMap.get(node) : null;
92
+ let privacyId = config.content ? Privacy.Sensitive : Privacy.Text
91
93
 
92
94
  if (parentId >= 0 && values[parentId]) {
93
95
  parentValue = values[parentId];
94
96
  parentValue.children.push(id);
95
97
  regionId = regionId === null ? parentValue.region : regionId;
96
98
  fragmentId = parentValue.fragment;
97
- privacy = parentValue.metadata.privacy;
99
+ fraudId = fraudId === null ? parentValue.metadata.fraud : fraudId;
98
100
  }
99
101
 
100
- // Check to see if this particular node should be masked or not
101
- privacy = getPrivacy(node, data, parentValue, privacy);
102
-
103
102
  // If there's an explicit region attribute set on the element, use it to mark a region on the page
104
103
  if (data.attributes && Constant.RegionData in data.attributes) {
105
104
  region.observe(node, data.attributes[Constant.RegionData]);
@@ -116,12 +115,13 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
116
115
  selector: null,
117
116
  hash: null,
118
117
  region: regionId,
119
- metadata: { active: true, suspend: false, privacy, position: null, size: null },
118
+ metadata: { active: true, suspend: false, privacy: privacyId, position: null, fraud: fraudId, size: null },
120
119
  fragment: fragmentId,
121
120
  };
122
121
 
122
+ privacy(node, values[id], parentValue);
123
123
  updateSelector(values[id]);
124
- size(values[id], parentValue);
124
+ size(values[id]);
125
125
  track(id, source, values[id].fragment);
126
126
  }
127
127
 
@@ -210,66 +210,63 @@ export function iframe(node: Node): HTMLIFrameElement {
210
210
  return doc && iframeMap.has(doc) ? iframeMap.get(doc) : null;
211
211
  }
212
212
 
213
- function getPrivacy(node: Node, data: NodeInfo, parent: NodeValue, privacy: Privacy): Privacy {
214
- let attributes = data.attributes;
213
+ function privacy(node: Node, value: NodeValue, parent: NodeValue): void {
214
+ let data = value.data;
215
+ let metadata = value.metadata;
216
+ let current = metadata.privacy;
217
+ let attributes = data.attributes || {};
215
218
  let tag = data.tag.toUpperCase();
216
219
 
217
- // If this node was explicitly configured to contain sensitive content, use that information and return the value
218
- if (privacyMap.has(node)) { return privacyMap.get(node); }
219
-
220
- // If it's a text node belonging to a STYLE or TITLE tag;
221
- // Or, the text node belongs to one of SCRUB_EXCEPTIONS
222
- // then reset the privacy setting to ensure we capture the content
223
- if (tag === Constant.TextTag && parent && parent.data) {
224
- let path = parent.selector ? parent.selector[Selector.Stable] : Constant.Empty;
225
- privacy = parent.data.tag === Constant.StyleTag || parent.data.tag === Constant.TitleTag ? Privacy.None : privacy;
226
- for (let entry of override) {
227
- if (path.indexOf(entry) >= 0) {
228
- privacy = Privacy.None;
229
- break;
230
- }
231
- }
232
- }
233
-
234
- // Do not proceed if attributes are missing for the node
235
- if (attributes === null || attributes === undefined) { return privacy; }
236
-
237
- // Look up for sensitive fields
238
- if (Constant.Class in attributes && privacy === Privacy.Sensitive) {
239
- for (let match of DISALLOWED_MATCH) {
240
- if (attributes[Constant.Class].indexOf(match) >= 0) {
241
- privacy = Privacy.Text;
242
- break;
243
- }
244
- }
245
- }
246
-
247
- // Check for disallowed list of fields (e.g. address, phone, etc.) only if the input node is not already masked
248
- if (tag === Constant.InputTag) {
249
- if (privacy === Privacy.None) {
220
+ switch (true) {
221
+ case Constant.MaskData in attributes:
222
+ metadata.privacy = Privacy.TextImage;
223
+ break;
224
+ case Constant.UnmaskData in attributes:
225
+ metadata.privacy = Privacy.None;
226
+ break;
227
+ case privacyMap.has(node):
228
+ // If this node was explicitly configured to contain sensitive content, honor that privacy setting
229
+ metadata.privacy = privacyMap.get(node);
230
+ break;
231
+ case fraudMap.has(node):
232
+ // If this node was explicitly configured to be evaluated for fraud, then also mask content
233
+ metadata.privacy = Privacy.Text;
234
+ break;
235
+ case tag === Constant.TextTag:
236
+ // If it's a text node belonging to a STYLE or TITLE tag or one of SCRUB_EXCEPTIONS, then capture content
237
+ let pTag = parent && parent.data ? parent.data.tag : Constant.Empty;
238
+ let pSelector = parent && parent.selector ? parent.selector[Selector.Stable] : Constant.Empty;
239
+ metadata.privacy = pTag === Constant.StyleTag || pTag === Constant.TitleTag || override.some(x => pSelector.indexOf(x) >= 0) ? Privacy.None : current;
240
+ break;
241
+ case Constant.Type in attributes:
242
+ // If this node has an explicit type assigned to it, go through masking rules to determine right privacy setting
243
+ metadata.privacy = inspect(attributes[Constant.Type], metadata);
244
+ break;
245
+ case tag === Constant.InputTag && current === Privacy.None:
246
+ // If even default privacy setting is to not mask, we still scan through input fields for any sensitive information
250
247
  let field: string = Constant.Empty;
251
- // Be aggressive in looking up any attribute (id, class, name, etc.) for disallowed names
252
- for (const attribute of Object.keys(attributes)) { field += attributes[attribute].toLowerCase(); }
253
- for (let name of DISALLOWED_NAMES) {
254
- if (field.indexOf(name) >= 0) {
255
- privacy = Privacy.Text;
256
- break;
257
- }
258
- }
259
- } else if (privacy === Privacy.Sensitive) {
260
- // Mask all input fields with an exception of type=submit; since they do not accept user input
261
- privacy = attributes && attributes[Constant.Type] === Constant.Submit ? Privacy.None : Privacy.Text;
262
- }
248
+ Object.keys(attributes).forEach(x => field += attributes[x].toLowerCase());
249
+ metadata.privacy = inspect(field, metadata);
250
+ break;
251
+ case current === Privacy.Sensitive && tag === Constant.InputTag:
252
+ // If it's a button or an input option, make an exception to disable masking
253
+ metadata.privacy = maskDisable.indexOf(attributes[Constant.Type]) >= 0 ? Privacy.None : current;
254
+ break;
255
+ case current === Privacy.Sensitive:
256
+ // In a mode where we mask sensitive information by default, look through class names to aggressively mask content
257
+ metadata.privacy = inspect(attributes[Constant.Class], metadata);
258
+ break;
259
+ default:
260
+ metadata.privacy = parent ? parent.metadata.privacy : metadata.privacy;
261
+ break;
263
262
  }
263
+ }
264
264
 
265
- // Check for disallowed list of types (e.g. password, email, etc.) and set the masked property appropriately
266
- if (Constant.Type in attributes && DISALLOWED_TYPES.indexOf(attributes[Constant.Type]) >= 0) { privacy = Privacy.Text; }
267
-
268
- // Following two conditions supersede any of the above. If there are explicit instructions to mask / unmask a field, we honor that.
269
- if (Constant.MaskData in attributes) { privacy = Privacy.TextImage; }
270
- if (Constant.UnmaskData in attributes) { privacy = Privacy.None; }
271
-
272
- return privacy;
265
+ function inspect(input: string, metadata: NodeMeta): Privacy {
266
+ if (input && maskText.some(x => input.indexOf(x) >= 0)) {
267
+ return Privacy.Text;
268
+ }
269
+ return metadata.privacy;
273
270
  }
274
271
 
275
272
  function diff(a: NodeInfo, b: NodeInfo, field: string): boolean {
@@ -358,19 +355,10 @@ function remove(id: number, source: Source): void {
358
355
  }
359
356
  }
360
357
 
361
- function size(value: NodeValue, parent: NodeValue): void {
362
- let data = value.data;
363
- let tag = data.tag;
364
-
365
- // If this element is a text node, is masked, and longer than configured length, then track box model for the parent element
366
- let isLongText = tag === Constant.TextTag && data.value && data.value.length > Setting.ResizeObserverThreshold;
367
- let isMasked = value.metadata.privacy === Privacy.Text || value.metadata.privacy === Privacy.TextImage;
368
- if (isLongText && isMasked && parent && parent.metadata.size === null) { parent.metadata.size = []; }
369
-
358
+ function size(value: NodeValue): void {
370
359
  // If this element is a image node, and is masked, then track box model for the current element
371
- if (data.tag === Constant.ImageTag && value.metadata.privacy === Privacy.TextImage) { value.metadata.size = []; }
360
+ if (value.data.tag === Constant.ImageTag && value.metadata.privacy === Privacy.TextImage) { value.metadata.size = []; }
372
361
  }
373
-
374
362
  function getPreviousId(node: Node): number {
375
363
  let id = null;
376
364