clarity-js 0.6.43 → 0.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-js",
3
- "version": "0.6.43",
3
+ "version": "0.7.0",
4
4
  "description": "An analytics library that uses web page interactions to generate aggregated insights",
5
5
  "author": "Microsoft Corp.",
6
6
  "license": "MIT",
@@ -6,12 +6,14 @@ let config: Config = {
6
6
  lean: false,
7
7
  track: true,
8
8
  content: true,
9
+ drop: [],
9
10
  mask: [],
10
11
  unmask: [],
11
12
  regions: [],
12
13
  extract: [],
13
14
  cookies: [],
14
- fraud: [],
15
+ fraud: true,
16
+ checksum: [],
15
17
  report: null,
16
18
  upload: null,
17
19
  fallback: null,
package/src/core/hash.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // tslint:disable: no-bitwise
2
- export default function(input: string): string {
2
+ export default function(input: string, precision: number = null): string {
3
3
  // Code inspired from C# GetHashCode: https://github.com/Microsoft/referencesource/blob/master/mscorlib/system/string.cs
4
4
  let hash = 0;
5
5
  let hashOne = 5381;
@@ -15,5 +15,5 @@ export default function(input: string): string {
15
15
  // Replace the magic number from C# implementation (1566083941) with a smaller prime number (11579)
16
16
  // This ensures we don't hit integer overflow and prevent collisions
17
17
  hash = Math.abs(hashOne + (hashTwo * 11579));
18
- return hash.toString(36);
18
+ return (precision ? hash % Math.pow(2, precision) : hash).toString(36);
19
19
  }
package/src/core/scrub.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Privacy } from "@clarity-types/core";
2
2
  import * as Data from "@clarity-types/data";
3
3
  import * as Layout from "@clarity-types/layout";
4
+ import config from "@src/core/config";
4
5
 
5
6
  const catchallRegex = /\S/gi;
6
7
  let unicodeRegex = true;
@@ -8,7 +9,7 @@ let digitRegex = null;
8
9
  let letterRegex = null;
9
10
  let currencyRegex = null;
10
11
 
11
- export default function(value: string, hint: string, privacy: Privacy, mangle: boolean = false): string {
12
+ export function text(value: string, hint: string, privacy: Privacy, mangle: boolean = false): string {
12
13
  if (value) {
13
14
  switch (privacy) {
14
15
  case Privacy.None:
@@ -19,8 +20,10 @@ export default function(value: string, hint: string, privacy: Privacy, mangle: b
19
20
  case "value":
20
21
  case "placeholder":
21
22
  case "click":
22
- case "input":
23
23
  return redact(value);
24
+ case "input":
25
+ case "change":
26
+ return mangleToken(value);
24
27
  }
25
28
  return value;
26
29
  case Privacy.Text:
@@ -36,16 +39,37 @@ export default function(value: string, hint: string, privacy: Privacy, mangle: b
36
39
  case "value":
37
40
  case "click":
38
41
  case "input":
42
+ case "change":
39
43
  return mangleToken(value);
40
44
  case "placeholder":
41
45
  return mask(value);
42
46
  }
43
47
  break;
48
+ case Privacy.Exclude:
49
+ switch (hint) {
50
+ case "value":
51
+ case "input":
52
+ case "click":
53
+ case "change":
54
+ return Array(Data.Setting.WordLength).join(Data.Constant.Mask);
55
+ case "checksum":
56
+ return Data.Constant.Empty;
57
+ }
44
58
  }
45
59
  }
46
60
  return value;
47
61
  }
48
62
 
63
+ export function url(input: string): string {
64
+ let drop = config.drop;
65
+ if (drop && drop.length > 0 && input && input.indexOf("?") > 0) {
66
+ let [path, query] = input.split("?");
67
+ let swap = Data.Constant.Dropped;
68
+ return path + "?" + query.split("&").map(p => drop.some(x => p.indexOf(`${x}=`) === 0) ? `${p.split("=")[0]}=${swap}` : p).join("&");
69
+ }
70
+ return input;
71
+ }
72
+
49
73
  function mangleText(value: string): string {
50
74
  let trimmed = value.trim();
51
75
  if (trimmed.length > 0) {
package/src/core/time.ts CHANGED
@@ -4,8 +4,8 @@ export function start(): void {
4
4
  startTime = performance.now();
5
5
  }
6
6
 
7
- export function time(ts: number = null): number {
8
- ts = ts ? ts : performance.now();
7
+ export function time(event: UIEvent = null): number {
8
+ let ts = event && event.timeStamp > 0 ? event.timeStamp : performance.now();
9
9
  return Math.max(Math.round(ts - startTime), 0);
10
10
  }
11
11
 
@@ -1,2 +1,2 @@
1
- let version = "0.6.43";
1
+ let version = "0.7.0";
2
2
  export default version;
@@ -3,6 +3,7 @@ import { BooleanFlag, Constant, Dimension, Metadata, MetadataCallback, MetadataC
3
3
  import * as core from "@src/core";
4
4
  import config from "@src/core/config";
5
5
  import hash from "@src/core/hash";
6
+ import * as scrub from "@src/core/scrub";
6
7
  import * as dimension from "@src/data/dimension";
7
8
  import * as metric from "@src/data/metric";
8
9
  import { set } from "@src/data/variable";
@@ -19,32 +20,35 @@ export function start(): void {
19
20
  // Populate ids for this page
20
21
  let s = session();
21
22
  let u = user();
22
- data = {
23
- projectId: config.projectId || hash(location.host),
24
- userId: u.id,
25
- sessionId: s.session,
26
- pageNum: s.count
27
- }
23
+ let projectId = config.projectId || hash(location.host);
24
+ data = { projectId, userId: u.id, sessionId: s.session, pageNum: s.count };
28
25
 
29
26
  // Override configuration based on what's in the session storage, unless it is blank (e.g. using upload callback, like in devtools)
30
27
  config.lean = config.track && s.upgrade !== null ? s.upgrade === BooleanFlag.False : config.lean;
31
28
  config.upload = config.track && typeof config.upload === Constant.String && s.upload && s.upload.length > Constant.HTTPS.length ? s.upload : config.upload;
32
- // Log dimensions
29
+
30
+ // Log page metadata as dimensions
33
31
  dimension.log(Dimension.UserAgent, ua);
34
32
  dimension.log(Dimension.PageTitle, title);
35
- dimension.log(Dimension.Url, location.href);
33
+ dimension.log(Dimension.Url, scrub.url(location.href));
36
34
  dimension.log(Dimension.Referrer, document.referrer);
37
35
  dimension.log(Dimension.TabId, tab());
38
36
  dimension.log(Dimension.PageLanguage, document.documentElement.lang);
39
37
  dimension.log(Dimension.DocumentDirection, document.dir);
38
+ dimension.log(Dimension.DevicePixelRatio, `${window.devicePixelRatio}`);
39
+
40
+ // Capture additional metadata as metrics
41
+ metric.max(Metric.ClientTimestamp, s.ts);
42
+ metric.max(Metric.Playback, BooleanFlag.False);
43
+
44
+ // Capture navigator specific dimensions
40
45
  if (navigator) {
41
- dimension.log(Dimension.Language, (<any>navigator).userLanguage || navigator.language);
46
+ dimension.log(Dimension.Language, navigator.language);
47
+ metric.max(Metric.HardwareConcurrency, navigator.hardwareConcurrency);
48
+ metric.max(Metric.MaxTouchPoints, navigator.maxTouchPoints);
49
+ metric.max(Metric.DeviceMemory, Math.round((<any>navigator).deviceMemory));
42
50
  userAgentData();
43
- }
44
-
45
- // Metrics
46
- metric.max(Metric.ClientTimestamp, s.ts);
47
- metric.max(Metric.Playback, BooleanFlag.False);
51
+ }
48
52
 
49
53
  if (screen) {
50
54
  metric.max(Metric.ScreenWidth, Math.round(screen.width));
@@ -63,22 +67,16 @@ export function start(): void {
63
67
  }
64
68
 
65
69
  function userAgentData(): void {
66
- if (navigator["userAgentData"] && navigator["userAgentData"].getHighEntropyValues) {
67
- navigator["userAgentData"].getHighEntropyValues(
68
- ["model",
69
- "platform",
70
- "platformVersion",
71
- "uaFullVersion"])
72
- .then(ua => {
73
- dimension.log(Dimension.Platform, ua.platform);
74
- dimension.log(Dimension.PlatformVersion, ua.platformVersion);
75
- ua.brands?.forEach(brand => {
76
- dimension.log(Dimension.Brand, brand.name + Constant.Tilde + brand.version);
77
- });
78
- dimension.log(Dimension.Model, ua.model);
79
- metric.max(Metric.Mobile, ua.mobile ? BooleanFlag.True : BooleanFlag.False);
80
- });
81
- }
70
+ let uaData = navigator["userAgentData"];
71
+ if (uaData && uaData.getHighEntropyValues) {
72
+ uaData.getHighEntropyValues(["model","platform","platformVersion","uaFullVersion"]).then(ua => {
73
+ dimension.log(Dimension.Platform, ua.platform);
74
+ dimension.log(Dimension.PlatformVersion, ua.platformVersion);
75
+ ua.brands?.forEach(brand => { dimension.log(Dimension.Brand, brand.name + Constant.Tilde + brand.version); });
76
+ dimension.log(Dimension.Model, ua.model);
77
+ metric.max(Metric.Mobile, ua.mobile ? BooleanFlag.True : BooleanFlag.False);
78
+ });
79
+ } else { dimension.log(Dimension.Platform, navigator.platform); }
82
80
  }
83
81
 
84
82
  export function stop(): void {
@@ -91,7 +89,6 @@ export function metadata(cb: MetadataCallback, wait: boolean = true): void {
91
89
  // Immediately invoke the callback if the caller explicitly doesn't want to wait for the upgrade confirmation
92
90
  cb(data, !config.lean);
93
91
  }
94
-
95
92
  callbacks.push({callback: cb, wait: wait });
96
93
  }
97
94
 
@@ -1,4 +1,5 @@
1
1
  import { Event, Token } from "@clarity-types/data";
2
+ import * as scrub from "@src/core/scrub";
2
3
  import { time } from "@src/core/time";
3
4
  import { queue } from "@src/data/upload";
4
5
  import * as fraud from "@src/diagnostic/fraud";
@@ -14,7 +15,7 @@ export default async function (type: Event): Promise<void> {
14
15
  tokens.push(script.data.line);
15
16
  tokens.push(script.data.column);
16
17
  tokens.push(script.data.stack);
17
- tokens.push(script.data.source);
18
+ tokens.push(scrub.url(script.data.source));
18
19
  queue(tokens);
19
20
  break;
20
21
  case Event.Log:
@@ -31,7 +32,7 @@ export default async function (type: Event): Promise<void> {
31
32
  if (fraud.data) {
32
33
  tokens.push(fraud.data.id);
33
34
  tokens.push(fraud.data.target);
34
- tokens.push(fraud.data.hash);
35
+ tokens.push(fraud.data.checksum);
35
36
  queue(tokens, false);
36
37
  }
37
38
  break;
@@ -1,5 +1,6 @@
1
1
  import { BooleanFlag, Event, IframeStatus, Metric, Setting } from "@clarity-types/data";
2
2
  import { FraudData } from "@clarity-types/diagnostic";
3
+ import config from "@src/core/config";
3
4
  import hash from "@src/core/hash";
4
5
  import * as metric from "@src/data/metric";
5
6
  import encode from "./encode";
@@ -19,12 +20,12 @@ export function start(): void {
19
20
  }
20
21
 
21
22
  export function check(id: number, target: number, input: string): void {
22
- // Compute hash for fraud detection. Hash is computed only if input meets the minimum length criteria
23
- if (id !== null && input && input.length >= Setting.WordLength) {
24
- data = { id, target, hash: hash(input) };
23
+ // Compute hash for fraud detection, if enabled. Hash is computed only if input meets the minimum length criteria
24
+ if (config.fraud && id !== null && input && input.length >= Setting.WordLength) {
25
+ data = { id, target, checksum: hash(input, Setting.ChecksumPrecision) };
25
26
  // Only encode this event if we haven't already reported this hash
26
- if (history.indexOf(data.hash) < 0) {
27
- history.push(data.hash);
27
+ if (history.indexOf(data.checksum) < 0) {
28
+ history.push(data.checksum);
28
29
  encode(Event.Fraud);
29
30
  }
30
31
  }
@@ -0,0 +1,37 @@
1
+ import { Constant, Event, Setting } from "@clarity-types/data";
2
+ import { ChangeState } from "@clarity-types/interaction";
3
+ import config from "@src/core/config";
4
+ import { bind } from "@src/core/event";
5
+ import hash from "@src/core/hash";
6
+ import { schedule } from "@src/core/task";
7
+ import { time } from "@src/core/time";
8
+ import { target } from "@src/layout/target";
9
+ import encode from "./encode";
10
+
11
+ export let state: ChangeState[] = [];
12
+
13
+ export function start(): void {
14
+ reset();
15
+ }
16
+
17
+ export function observe(root: Node): void {
18
+ bind(root, "change", recompute, true);
19
+ }
20
+
21
+ function recompute(evt: UIEvent): void {
22
+ let element = target(evt) as HTMLInputElement;
23
+ if (element) {
24
+ let value = element.value;
25
+ let checksum = value && value.length >= Setting.WordLength && config.fraud ? hash(value, Setting.ChecksumPrecision) : Constant.Empty;
26
+ state.push({ time: time(evt), event: Event.Change, data: { target: target(evt), type: element.type, value, checksum } });
27
+ schedule(encode.bind(this, Event.Change));
28
+ }
29
+ }
30
+
31
+ export function reset(): void {
32
+ state = [];
33
+ }
34
+
35
+ export function stop(): void {
36
+ reset();
37
+ }
@@ -54,7 +54,7 @@ function handler(event: Event, root: Node, evt: MouseEvent): void {
54
54
  // Check for null values before processing this event
55
55
  if (x !== null && y !== null) {
56
56
  state.push({
57
- time: time(), event, data: {
57
+ time: time(evt), event, data: {
58
58
  target: t,
59
59
  x,
60
60
  y,
@@ -19,7 +19,7 @@ export function observe(root: Node): void {
19
19
  }
20
20
 
21
21
  function recompute(action: Clipboard, evt: UIEvent): void {
22
- state.push({ time: time(), event: Event.Clipboard, data: { target: target(evt), action } });
22
+ state.push({ time: time(evt), event: Event.Clipboard, data: { target: target(evt), action } });
23
23
  schedule(encode.bind(this, Event.Clipboard));
24
24
  }
25
25
 
@@ -1,9 +1,10 @@
1
1
  import { Constant, Event, Token } from "@clarity-types/data";
2
- import scrub from "@src/core/scrub";
2
+ import * as scrub from "@src/core/scrub";
3
3
  import { time } from "@src/core/time";
4
4
  import * as baseline from "@src/data/baseline";
5
5
  import { queue } from "@src/data/upload";
6
6
  import { metadata } from "@src/layout/target";
7
+ import * as change from "./change";
7
8
  import * as click from "./click";
8
9
  import * as clipboard from "./clipboard";
9
10
  import * as input from "./input";
@@ -16,8 +17,8 @@ import * as timeline from "./timeline";
16
17
  import * as unload from "./unload";
17
18
  import * as visibility from "./visibility";
18
19
 
19
- export default async function (type: Event): Promise<void> {
20
- let t = time();
20
+ export default async function (type: Event, ts: number = null): Promise<void> {
21
+ let t = ts || time();
21
22
  let tokens: Token[] = [t, type];
22
23
  switch (type) {
23
24
  case Event.MouseDown:
@@ -55,8 +56,8 @@ export default async function (type: Event): Promise<void> {
55
56
  tokens.push(entry.data.button);
56
57
  tokens.push(entry.data.reaction);
57
58
  tokens.push(entry.data.context);
58
- tokens.push(scrub(entry.data.text, "click", cTarget.privacy));
59
- tokens.push(entry.data.link);
59
+ tokens.push(scrub.text(entry.data.text, "click", cTarget.privacy));
60
+ tokens.push(scrub.url(entry.data.link));
60
61
  tokens.push(cHash);
61
62
  tokens.push(entry.data.trust);
62
63
  queue(tokens);
@@ -95,7 +96,7 @@ export default async function (type: Event): Promise<void> {
95
96
  let iTarget = metadata(entry.data.target as Node, entry.event, entry.data.value);
96
97
  tokens = [entry.time, entry.event];
97
98
  tokens.push(iTarget.id);
98
- tokens.push(scrub(entry.data.value, "input", iTarget.privacy));
99
+ tokens.push(scrub.text(entry.data.value, "input", iTarget.privacy));
99
100
  queue(tokens);
100
101
  }
101
102
  input.reset();
@@ -127,6 +128,21 @@ export default async function (type: Event): Promise<void> {
127
128
  }
128
129
  scroll.reset();
129
130
  break;
131
+ case Event.Change:
132
+ for (let entry of change.state) {
133
+ tokens = [entry.time, entry.event];
134
+ let target = metadata(entry.data.target as Node, entry.event);
135
+ if (target.id > 0) {
136
+ tokens = [entry.time, entry.event];
137
+ tokens.push(target.id);
138
+ tokens.push(entry.data.type);
139
+ tokens.push(scrub.text(entry.data.value, "change", target.privacy));
140
+ tokens.push(scrub.text(entry.data.checksum, "checksum", target.privacy));
141
+ queue(tokens);
142
+ }
143
+ }
144
+ change.reset();
145
+ break;
130
146
  case Event.Submit:
131
147
  for (let entry of submit.state) {
132
148
  tokens = [entry.time, entry.event];
@@ -1,3 +1,4 @@
1
+ import * as change from "@src/interaction/change";
1
2
  import * as click from "@src/interaction/click";
2
3
  import * as clipboard from "@src/interaction/clipboard";
3
4
  import * as input from "@src/interaction/input";
@@ -20,6 +21,7 @@ export function start(): void {
20
21
  visibility.start();
21
22
  scroll.start();
22
23
  selection.start();
24
+ change.start();
23
25
  submit.start();
24
26
  unload.start();
25
27
  }
@@ -34,6 +36,7 @@ export function stop(): void {
34
36
  visibility.stop();
35
37
  scroll.stop();
36
38
  selection.stop();
39
+ change.stop();
37
40
  submit.stop();
38
41
  unload.stop()
39
42
  }
@@ -48,6 +51,7 @@ export function observe(root: Node): void {
48
51
  pointer.observe(root);
49
52
  input.observe(root);
50
53
  selection.observe(root);
54
+ change.observe(root);
51
55
  submit.observe(root);
52
56
  }
53
57
  }
@@ -36,10 +36,10 @@ function recompute(evt: UIEvent): void {
36
36
  // If last entry in the queue is for the same target node as the current one, remove it so we can later swap it with current data.
37
37
  if (state.length > 0 && (state[state.length - 1].data.target === data.target)) { state.pop(); }
38
38
 
39
- state.push({ time: time(), event: Event.Input, data });
39
+ state.push({ time: time(evt), event: Event.Input, data });
40
40
 
41
41
  clearTimeout(timeout);
42
- timeout = setTimeout(process, Setting.LookAhead, Event.Input);
42
+ timeout = setTimeout(process, Setting.InputLookAhead, Event.Input);
43
43
  }
44
44
  }
45
45
 
@@ -41,7 +41,7 @@ function mouse(event: Event, root: Node, evt: MouseEvent): void {
41
41
  }
42
42
 
43
43
  // Check for null values before processing this event
44
- if (x !== null && y !== null) { handler({ time: time(), event, data: { target: target(evt), x, y } }); }
44
+ if (x !== null && y !== null) { handler({ time: time(evt), event, data: { target: target(evt), x, y } }); }
45
45
  }
46
46
 
47
47
  function touch(event: Event, root: Node, evt: TouchEvent): void {
@@ -49,7 +49,7 @@ function touch(event: Event, root: Node, evt: TouchEvent): void {
49
49
  let d = frame ? frame.contentDocument.documentElement : document.documentElement;
50
50
  let touches = evt.changedTouches;
51
51
 
52
- let t = time();
52
+ let t = time(evt);
53
53
  if (touches) {
54
54
  for (let i = 0; i < touches.length; i++) {
55
55
  let entry = touches[i];
@@ -39,7 +39,7 @@ function recompute(event: UIEvent = null): void {
39
39
  // And, if for some reason that is not available, fall back to looking up scrollTop on document.documentElement.
40
40
  let x = element === de && "pageXOffset" in w ? Math.round(w.pageXOffset) : Math.round((element as HTMLElement).scrollLeft);
41
41
  let y = element === de && "pageYOffset" in w ? Math.round(w.pageYOffset) : Math.round((element as HTMLElement).scrollTop);
42
- let current: ScrollState = { time: time(), event: Event.Scroll, data: {target: element, x, y} };
42
+ let current: ScrollState = { time: time(event), event: Event.Scroll, data: {target: element, x, y} };
43
43
 
44
44
  // We don't send any scroll events if this is the first event and the current position is top (0,0)
45
45
  if ((event === null && x === 0 && y === 0) || (x === null || y === null)) { return; }
@@ -17,7 +17,7 @@ export function observe(root: Node): void {
17
17
  }
18
18
 
19
19
  function recompute(evt: UIEvent): void {
20
- state.push({ time: time(), event: Event.Submit, data: { target: target(evt) } });
20
+ state.push({ time: time(evt), event: Event.Submit, data: { target: target(evt) } });
21
21
  schedule(encode.bind(this, Event.Submit));
22
22
  }
23
23
 
@@ -2,6 +2,7 @@ import { Event } from "@clarity-types/data";
2
2
  import { UnloadData } from "@clarity-types/interaction";
3
3
  import * as clarity from "@src/clarity";
4
4
  import { bind } from "@src/core/event";
5
+ import { time } from "@src/core/time";
5
6
  import encode from "./encode";
6
7
 
7
8
  export let data: UnloadData;
@@ -12,7 +13,7 @@ export function start(): void {
12
13
 
13
14
  function recompute(evt: UIEvent): void {
14
15
  data = { name: evt.type };
15
- encode(Event.Unload);
16
+ encode(Event.Unload, time(evt));
16
17
  clarity.stop();
17
18
  }
18
19
 
@@ -1,6 +1,7 @@
1
1
  import { Event } from "@clarity-types/data";
2
2
  import { VisibilityData } from "@clarity-types/interaction";
3
3
  import { bind } from "@src/core/event";
4
+ import { time } from "@src/core/time";
4
5
  import encode from "./encode";
5
6
 
6
7
  export let data: VisibilityData;
@@ -10,9 +11,9 @@ export function start(): void {
10
11
  recompute();
11
12
  }
12
13
 
13
- function recompute(): void {
14
+ function recompute(evt: UIEvent = null): void {
14
15
  data = { visible: "visibilityState" in document ? document.visibilityState : "default" };
15
- encode(Event.Visibility);
16
+ encode(Event.Visibility, time(evt));
16
17
  }
17
18
 
18
19
  export function reset(): void {
package/src/layout/dom.ts CHANGED
@@ -17,8 +17,9 @@ let override = [];
17
17
  let unmask = [];
18
18
  let updatedFragments: { [fragment: number]: string } = {};
19
19
  let maskText = [];
20
- let maskInput = [];
20
+ let maskExclude = [];
21
21
  let maskDisable = [];
22
+ let maskTags = [];
22
23
 
23
24
  // The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced
24
25
  let idMap: WeakMap<Node, number> = null; // Maps node => id.
@@ -44,8 +45,9 @@ function reset(): void {
44
45
  override = [];
45
46
  unmask = [];
46
47
  maskText = Mask.Text.split(Constant.Comma);
47
- maskInput = Mask.Input.split(Constant.Comma);
48
+ maskExclude = Mask.Exclude.split(Constant.Comma);
48
49
  maskDisable = Mask.Disable.split(Constant.Comma);
50
+ maskTags = Mask.Tags.split(Constant.Comma);
49
51
  idMap = new WeakMap();
50
52
  iframeMap = new WeakMap();
51
53
  privacyMap = new WeakMap();
@@ -67,7 +69,7 @@ export function parse(root: ParentNode, init: boolean = false): void {
67
69
  if ("querySelectorAll" in root) {
68
70
  config.regions.forEach(x => root.querySelectorAll(x[1]).forEach(e => region.observe(e, `${x[0]}`))); // Regions
69
71
  config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements
70
- config.fraud.forEach(x => root.querySelectorAll(x[1]).forEach(e => fraudMap.set(e, x[0]))); // Fraud Check
72
+ config.checksum.forEach(x => root.querySelectorAll(x[1]).forEach(e => fraudMap.set(e, x[0]))); // Fraud Checksum Check
71
73
  unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements
72
74
  }
73
75
  } catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
@@ -221,6 +223,16 @@ function privacy(node: Node, value: NodeValue, parent: NodeValue): void {
221
223
  let tag = data.tag.toUpperCase();
222
224
 
223
225
  switch (true) {
226
+ case maskTags.indexOf(tag) >= 0:
227
+ let type = attributes[Constant.Type];
228
+ let meta: string = Constant.Empty;
229
+ Object.keys(attributes).forEach(x => meta += attributes[x].toLowerCase());
230
+ let exclude = maskExclude.some(x => meta.indexOf(x) >= 0);
231
+ // Regardless of privacy mode, always mask off user input from input boxes or drop downs with two exceptions:
232
+ // (1) The node is detected to be one of the excluded fields, in which case we drop everything
233
+ // (2) The node's type is one of the allowed types (like checkboxes)
234
+ metadata.privacy = tag === Constant.InputTag && maskDisable.indexOf(type) >= 0 ? current : (exclude ? Privacy.Exclude : Privacy.Text);
235
+ break;
224
236
  case Constant.MaskData in attributes:
225
237
  metadata.privacy = Privacy.TextImage;
226
238
  break;
@@ -242,20 +254,6 @@ function privacy(node: Node, value: NodeValue, parent: NodeValue): void {
242
254
  let tags : string[] = [Constant.StyleTag, Constant.TitleTag, Constant.SvgStyle];
243
255
  metadata.privacy = tags.includes(pTag) || override.some(x => pSelector.indexOf(x) >= 0) ? Privacy.None : current;
244
256
  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
247
- let field: string = Constant.Empty;
248
- Object.keys(attributes).forEach(x => field += attributes[x].toLowerCase());
249
- metadata.privacy = inspect(field, maskInput, metadata);
250
- break;
251
- case tag === Constant.InputTag && current === Privacy.Sensitive:
252
- // Look through class names to aggressively mask content
253
- metadata.privacy = inspect(attributes[Constant.Class], maskText, metadata);
254
- // If this node has an explicit type assigned to it, go through masking rules to determine right privacy setting
255
- metadata.privacy = inspect(attributes[Constant.Type], maskInput, metadata);
256
- // If it's a button or an input option, make an exception to disable masking in sensitive mode
257
- metadata.privacy = maskDisable.indexOf(attributes[Constant.Type]) >= 0 ? Privacy.None : metadata.privacy;
258
- break;
259
257
  case current === Privacy.Sensitive:
260
258
  // In a mode where we mask sensitive information by default, look through class names to aggressively mask content
261
259
  metadata.privacy = inspect(attributes[Constant.Class], maskText, metadata);
@@ -2,7 +2,7 @@ import { Privacy, Task, Timer } from "@clarity-types/core";
2
2
  import { Event, Setting, Token } from "@clarity-types/data";
3
3
  import { Constant, NodeInfo, NodeValue } from "@clarity-types/layout";
4
4
  import config from "@src/core/config";
5
- import scrub from "@src/core/scrub";
5
+ import * as scrub from "@src/core/scrub";
6
6
  import * as task from "@src/core/task";
7
7
  import { time } from "@src/core/time";
8
8
  import tokenize from "@src/data/token";
@@ -73,7 +73,7 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu
73
73
  break;
74
74
  case "value":
75
75
  fraud.check(value.metadata.fraud, value.id, data[key]);
76
- tokens.push(scrub(data[key], data.tag, privacy, mangle));
76
+ tokens.push(scrub.text(data[key], data.tag, privacy, mangle));
77
77
  break;
78
78
  }
79
79
  }
@@ -106,5 +106,5 @@ function str(input: number): string {
106
106
  }
107
107
 
108
108
  function attribute(key: string, value: string, privacy: Privacy): string {
109
- return `${key}=${scrub(value, key, privacy)}`;
109
+ return `${key}=${scrub.text(value, key, privacy)}`;
110
110
  }
@@ -127,9 +127,20 @@ export default function (node: Node, source: Source): Node {
127
127
  break;
128
128
  case "HEAD":
129
129
  let head = { tag, attributes };
130
- if (location) { head.attributes[Constant.Base] = location.protocol + "//" + location.hostname; }
130
+ let l = insideFrame && node.ownerDocument?.location ? node.ownerDocument.location : location;
131
+ head.attributes[Constant.Base] = l.protocol + "//" + l.hostname + l.pathname;
131
132
  dom[call](node, parent, head, source);
132
133
  break;
134
+ case "BASE":
135
+ // Override the auto detected base path to explicit value specified in this tag
136
+ let baseHead = dom.get(node.parentElement);
137
+ if (baseHead) {
138
+ // We create "a" element so we can generate protocol and hostname for relative paths like "/path/"
139
+ let a = document.createElement("a");
140
+ a.href = attributes["href"];
141
+ baseHead.data.attributes[Constant.Base] = a.protocol + "//" + a.hostname + a.pathname;
142
+ }
143
+ break;
133
144
  case "STYLE":
134
145
  let styleData = { tag, attributes, value: getStyleValue(element as HTMLStyleElement) };
135
146
  dom[call](node, parent, styleData, source);
@@ -12,6 +12,11 @@ let observer: PerformanceObserver;
12
12
  const types: string[] = [Constant.Navigation, Constant.Resource, Constant.LongTask, Constant.FID, Constant.CLS, Constant.LCP];
13
13
 
14
14
  export function start(): void {
15
+ // Capture connection properties, if available
16
+ if (navigator && "connection" in navigator) {
17
+ dimension.log(Dimension.ConnectionType, navigator["connection"]["effectiveType"]);
18
+ }
19
+
15
20
  // Check the browser support performance observer as a pre-requisite for any performance measurement
16
21
  if (window["PerformanceObserver"] && PerformanceObserver.supportedEntryTypes) {
17
22
  // Start monitoring performance data after page has finished loading.