clarity-js 0.6.34 → 0.6.37
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/build/clarity.js +679 -749
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +679 -749
- package/package.json +1 -1
- package/src/core/config.ts +1 -0
- package/src/core/history.ts +10 -3
- package/src/core/timeout.ts +1 -1
- package/src/core/version.ts +1 -1
- package/src/data/metadata.ts +1 -2
- package/src/data/metric.ts +3 -3
- package/src/diagnostic/encode.ts +9 -0
- package/src/diagnostic/fraud.ts +29 -0
- package/src/diagnostic/index.ts +2 -0
- package/src/diagnostic/script.ts +1 -11
- package/src/interaction/click.ts +2 -1
- package/src/interaction/encode.ts +4 -3
- package/src/interaction/input.ts +1 -8
- package/src/layout/dom.ts +71 -82
- package/src/layout/encode.ts +15 -15
- package/src/layout/index.ts +0 -3
- package/src/layout/mutation.ts +1 -1
- package/src/layout/node.ts +1 -1
- package/src/layout/target.ts +7 -4
- package/src/performance/observer.ts +10 -2
- package/test/core.test.ts +27 -18
- package/test/helper.ts +5 -0
- package/test/html/core.html +4 -1
- package/types/core.d.ts +2 -0
- package/types/data.d.ts +11 -5
- package/types/diagnostic.d.ts +6 -0
- package/types/interaction.d.ts +1 -0
- package/types/layout.d.ts +7 -6
- package/types/performance.d.ts +0 -7
- package/src/layout/box.ts +0 -83
package/package.json
CHANGED
package/src/core/config.ts
CHANGED
package/src/core/history.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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";
|
|
5
|
-
import * as
|
|
6
|
+
import * as metric from "@src/data/metric";
|
|
7
|
+
|
|
6
8
|
let pushState = null;
|
|
7
9
|
let replaceState = null;
|
|
8
10
|
let url = null;
|
|
@@ -50,10 +52,15 @@ function compute(): void {
|
|
|
50
52
|
if (url !== getCurrentUrl()) {
|
|
51
53
|
// If the url changed, start tracking it as a new page
|
|
52
54
|
clarity.stop();
|
|
53
|
-
window.setTimeout(
|
|
55
|
+
window.setTimeout(restart, Setting.RestartDelay);
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
function restart(): void {
|
|
60
|
+
clarity.start();
|
|
61
|
+
metric.max(Metric.SinglePage, BooleanFlag.True);
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
function getCurrentUrl(): string {
|
|
58
65
|
return location.href ? location.href.replace(location.hash, Constant.Empty) : location.href;
|
|
59
66
|
}
|
package/src/core/timeout.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/src/core/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let version = "0.6.
|
|
1
|
+
let version = "0.6.37";
|
|
2
2
|
export default version;
|
package/src/data/metadata.ts
CHANGED
|
@@ -39,7 +39,6 @@ export function start(): void {
|
|
|
39
39
|
dimension.log(Dimension.DocumentDirection, document.dir);
|
|
40
40
|
if (navigator) {
|
|
41
41
|
dimension.log(Dimension.Language, (<any>navigator).userLanguage || navigator.language);
|
|
42
|
-
metric.max(Metric.Automation, navigator.webdriver ? BooleanFlag.True : BooleanFlag.False);
|
|
43
42
|
userAgentData();
|
|
44
43
|
}
|
|
45
44
|
|
|
@@ -63,7 +62,7 @@ export function start(): void {
|
|
|
63
62
|
track(u);
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
function userAgentData(): void {
|
|
67
66
|
if (navigator["userAgentData"] && navigator["userAgentData"].getHighEntropyValues) {
|
|
68
67
|
navigator["userAgentData"].getHighEntropyValues(
|
|
69
68
|
["model",
|
package/src/data/metric.ts
CHANGED
|
@@ -15,11 +15,11 @@ export function stop(): void {
|
|
|
15
15
|
updates = {};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function count(metric: Metric
|
|
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]
|
|
22
|
-
updates[metric]
|
|
21
|
+
data[metric]++;
|
|
22
|
+
updates[metric]++;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function sum(metric: Metric, value: number): void {
|
package/src/diagnostic/encode.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/diagnostic/index.ts
CHANGED
package/src/diagnostic/script.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
|
package/src/interaction/click.ts
CHANGED
|
@@ -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();
|
package/src/interaction/input.ts
CHANGED
|
@@ -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,
|
|
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,21 @@ 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
|
|
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
|
-
|
|
99
|
+
fraudId = fraudId === null ? parentValue.metadata.fraud : fraudId;
|
|
100
|
+
privacyId = parentValue.metadata.privacy;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
// Check to see if this particular node should be masked or not
|
|
101
|
-
privacy = getPrivacy(node, data, parentValue, privacy);
|
|
102
|
-
|
|
103
103
|
// If there's an explicit region attribute set on the element, use it to mark a region on the page
|
|
104
104
|
if (data.attributes && Constant.RegionData in data.attributes) {
|
|
105
105
|
region.observe(node, data.attributes[Constant.RegionData]);
|
|
@@ -116,12 +116,13 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
|
|
|
116
116
|
selector: null,
|
|
117
117
|
hash: null,
|
|
118
118
|
region: regionId,
|
|
119
|
-
metadata: { active: true, suspend: false, privacy, position: null, size: null },
|
|
119
|
+
metadata: { active: true, suspend: false, privacy: privacyId, position: null, fraud: fraudId, size: null },
|
|
120
120
|
fragment: fragmentId,
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
+
privacy(node, values[id], parentValue);
|
|
123
124
|
updateSelector(values[id]);
|
|
124
|
-
size(values[id]
|
|
125
|
+
size(values[id]);
|
|
125
126
|
track(id, source, values[id].fragment);
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -210,66 +211,63 @@ export function iframe(node: Node): HTMLIFrameElement {
|
|
|
210
211
|
return doc && iframeMap.has(doc) ? iframeMap.get(doc) : null;
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
function
|
|
214
|
-
let
|
|
214
|
+
function privacy(node: Node, value: NodeValue, parent: NodeValue): void {
|
|
215
|
+
let data = value.data;
|
|
216
|
+
let metadata = value.metadata;
|
|
217
|
+
let current = metadata.privacy;
|
|
218
|
+
let attributes = data.attributes || {};
|
|
215
219
|
let tag = data.tag.toUpperCase();
|
|
216
220
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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) {
|
|
221
|
+
switch (true) {
|
|
222
|
+
case Constant.MaskData in attributes:
|
|
223
|
+
metadata.privacy = Privacy.TextImage;
|
|
224
|
+
break;
|
|
225
|
+
case Constant.UnmaskData in attributes:
|
|
226
|
+
metadata.privacy = Privacy.None;
|
|
227
|
+
break;
|
|
228
|
+
case privacyMap.has(node):
|
|
229
|
+
// If this node was explicitly configured to contain sensitive content, honor that privacy setting
|
|
230
|
+
metadata.privacy = privacyMap.get(node);
|
|
231
|
+
break;
|
|
232
|
+
case fraudMap.has(node):
|
|
233
|
+
// If this node was explicitly configured to be evaluated for fraud, then also mask content
|
|
234
|
+
metadata.privacy = Privacy.Text;
|
|
235
|
+
break;
|
|
236
|
+
case tag === Constant.TextTag:
|
|
237
|
+
// If it's a text node belonging to a STYLE or TITLE tag or one of SCRUB_EXCEPTIONS, then capture content
|
|
238
|
+
let pTag = parent && parent.data ? parent.data.tag : Constant.Empty;
|
|
239
|
+
let pSelector = parent && parent.selector ? parent.selector[Selector.Stable] : Constant.Empty;
|
|
240
|
+
metadata.privacy = pTag === Constant.StyleTag || pTag === Constant.TitleTag || override.some(x => pSelector.indexOf(x) >= 0) ? Privacy.None : current;
|
|
241
|
+
break;
|
|
242
|
+
case Constant.Type in attributes:
|
|
243
|
+
// If this node has an explicit type assigned to it, go through masking rules to determine right privacy setting
|
|
244
|
+
metadata.privacy = inspect(attributes[Constant.Type], metadata);
|
|
245
|
+
break;
|
|
246
|
+
case tag === Constant.InputTag && current === Privacy.None:
|
|
247
|
+
// If even default privacy setting is to not mask, we still scan through input fields for any sensitive information
|
|
250
248
|
let field: string = Constant.Empty;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
Object.keys(attributes).forEach(x => field += attributes[x].toLowerCase());
|
|
250
|
+
metadata.privacy = inspect(field, metadata);
|
|
251
|
+
break;
|
|
252
|
+
case current === Privacy.Sensitive && tag === Constant.InputTag:
|
|
253
|
+
// If it's a button or an input option, make an exception to disable masking
|
|
254
|
+
metadata.privacy = maskDisable.indexOf(attributes[Constant.Type]) >= 0 ? Privacy.None : current;
|
|
255
|
+
break;
|
|
256
|
+
case current === Privacy.Sensitive:
|
|
257
|
+
// In a mode where we mask sensitive information by default, look through class names to aggressively mask content
|
|
258
|
+
metadata.privacy = inspect(attributes[Constant.Class], metadata);
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
metadata.privacy = parent ? parent.metadata.privacy : metadata.privacy;
|
|
262
|
+
break;
|
|
263
263
|
}
|
|
264
|
+
}
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (Constant.UnmaskData in attributes) { privacy = Privacy.None; }
|
|
271
|
-
|
|
272
|
-
return privacy;
|
|
266
|
+
function inspect(input: string, metadata: NodeMeta): Privacy {
|
|
267
|
+
if (input && maskText.some(x => input.indexOf(x) >= 0)) {
|
|
268
|
+
return Privacy.Text;
|
|
269
|
+
}
|
|
270
|
+
return metadata.privacy;
|
|
273
271
|
}
|
|
274
272
|
|
|
275
273
|
function diff(a: NodeInfo, b: NodeInfo, field: string): boolean {
|
|
@@ -358,19 +356,10 @@ function remove(id: number, source: Source): void {
|
|
|
358
356
|
}
|
|
359
357
|
}
|
|
360
358
|
|
|
361
|
-
function size(value: NodeValue
|
|
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
|
-
|
|
359
|
+
function size(value: NodeValue): void {
|
|
370
360
|
// 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 = []; }
|
|
361
|
+
if (value.data.tag === Constant.ImageTag && value.metadata.privacy === Privacy.TextImage) { value.metadata.size = []; }
|
|
372
362
|
}
|
|
373
|
-
|
|
374
363
|
function getPreviousId(node: Node): number {
|
|
375
364
|
let id = null;
|
|
376
365
|
|
package/src/layout/encode.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Privacy, Task, Timer } from "@clarity-types/core";
|
|
2
|
-
import { Event, Token } from "@clarity-types/data";
|
|
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
5
|
import scrub from "@src/core/scrub";
|
|
@@ -8,7 +8,7 @@ import { time } from "@src/core/time";
|
|
|
8
8
|
import tokenize from "@src/data/token";
|
|
9
9
|
import * as baseline from "@src/data/baseline";
|
|
10
10
|
import { queue } from "@src/data/upload";
|
|
11
|
-
import * as
|
|
11
|
+
import * as fraud from "@src/diagnostic/fraud";
|
|
12
12
|
import * as doc from "./document";
|
|
13
13
|
import * as dom from "./dom";
|
|
14
14
|
import * as region from "./region";
|
|
@@ -35,16 +35,6 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu
|
|
|
35
35
|
}
|
|
36
36
|
region.reset();
|
|
37
37
|
break;
|
|
38
|
-
case Event.Box:
|
|
39
|
-
let b = box.data;
|
|
40
|
-
for (let entry of b) {
|
|
41
|
-
tokens.push(entry.id);
|
|
42
|
-
tokens.push(entry.width);
|
|
43
|
-
tokens.push(entry.height);
|
|
44
|
-
}
|
|
45
|
-
box.reset();
|
|
46
|
-
queue(tokens);
|
|
47
|
-
break;
|
|
48
38
|
case Event.Discover:
|
|
49
39
|
case Event.Mutation:
|
|
50
40
|
// Check if we are operating within the context of the current page
|
|
@@ -62,18 +52,17 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu
|
|
|
62
52
|
let privacy = value.metadata.privacy;
|
|
63
53
|
let mangle = shouldMangle(value);
|
|
64
54
|
let keys = active ? ["tag", "attributes", "value"] : ["tag"];
|
|
65
|
-
box.compute(value.id);
|
|
66
55
|
for (let key of keys) {
|
|
67
56
|
if (data[key]) {
|
|
68
57
|
switch (key) {
|
|
69
58
|
case "tag":
|
|
70
|
-
let
|
|
59
|
+
let box = size(value);
|
|
71
60
|
let factor = mangle ? -1 : 1;
|
|
72
61
|
tokens.push(value.id * factor);
|
|
73
62
|
if (value.parent && active) { tokens.push(value.parent); }
|
|
74
63
|
if (value.previous && active) { tokens.push(value.previous); }
|
|
75
64
|
tokens.push(suspend ? Constant.SuspendMutationTag : data[key]);
|
|
76
|
-
if (
|
|
65
|
+
if (box && box.length === 2) { tokens.push(`${Constant.Box}${str(box[0])}.${str(box[1])}`); }
|
|
77
66
|
break;
|
|
78
67
|
case "attributes":
|
|
79
68
|
for (let attr in data[key]) {
|
|
@@ -83,6 +72,7 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu
|
|
|
83
72
|
}
|
|
84
73
|
break;
|
|
85
74
|
case "value":
|
|
75
|
+
fraud.check(value.metadata.fraud, value.id, data[key]);
|
|
86
76
|
tokens.push(scrub(data[key], data.tag, privacy, mangle));
|
|
87
77
|
break;
|
|
88
78
|
}
|
|
@@ -101,6 +91,16 @@ function shouldMangle(value: NodeValue): boolean {
|
|
|
101
91
|
return value.data.tag === Constant.TextTag && !(privacy === Privacy.None || privacy === Privacy.Sensitive);
|
|
102
92
|
}
|
|
103
93
|
|
|
94
|
+
function size(value: NodeValue): number[] {
|
|
95
|
+
if (value.metadata.size !== null && value.metadata.size.length === 0) {
|
|
96
|
+
let img = dom.getNode(value.id) as HTMLImageElement;
|
|
97
|
+
if (img) {
|
|
98
|
+
return [Math.floor(img.offsetWidth * Setting.BoxPrecision), Math.floor(img.offsetHeight * Setting.BoxPrecision)];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return value.metadata.size;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
104
|
function str(input: number): string {
|
|
105
105
|
return input.toString(36);
|
|
106
106
|
}
|
package/src/layout/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as box from "@src/layout/box";
|
|
2
1
|
import * as discover from "@src/layout/discover";
|
|
3
2
|
import * as doc from "@src/layout/document";
|
|
4
3
|
import * as dom from "@src/layout/dom";
|
|
@@ -13,13 +12,11 @@ export function start(): void {
|
|
|
13
12
|
dom.start();
|
|
14
13
|
mutation.start();
|
|
15
14
|
discover.start();
|
|
16
|
-
box.start();
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
export function stop(): void {
|
|
20
18
|
region.stop();
|
|
21
19
|
dom.stop();
|
|
22
20
|
mutation.stop();
|
|
23
|
-
box.stop();
|
|
24
21
|
doc.end();
|
|
25
22
|
}
|
package/src/layout/mutation.ts
CHANGED
|
@@ -114,7 +114,7 @@ function handle(m: MutationRecord[]): void {
|
|
|
114
114
|
summary.track(Event.Mutation, now);
|
|
115
115
|
mutations.push({ time: now, mutations: m});
|
|
116
116
|
task.schedule(process, Priority.High).then((): void => {
|
|
117
|
-
|
|
117
|
+
setTimeout(doc.compute)
|
|
118
118
|
measure(region.compute)();
|
|
119
119
|
});
|
|
120
120
|
}
|
package/src/layout/node.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as interaction from "@src/interaction";
|
|
|
7
7
|
import * as mutation from "@src/layout/mutation";
|
|
8
8
|
import * as schema from "@src/layout/schema";
|
|
9
9
|
|
|
10
|
-
const IGNORE_ATTRIBUTES = ["title", "alt", "onload", "onfocus", "onerror"];
|
|
10
|
+
const IGNORE_ATTRIBUTES = ["title", "alt", "onload", "onfocus", "onerror", "data-drupal-form-submit-last"];
|
|
11
11
|
const newlineRegex = /[\r\n]+/g;
|
|
12
12
|
|
|
13
13
|
export default function (node: Node, source: Source): Node {
|
package/src/layout/target.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Privacy } from "@clarity-types/core";
|
|
2
2
|
import { Event } from "@clarity-types/data";
|
|
3
3
|
import { TargetMetadata } from "@clarity-types/layout";
|
|
4
|
-
import
|
|
4
|
+
import * as fraud from "@src/diagnostic/fraud";
|
|
5
|
+
import * as region from "@src/layout/region";
|
|
5
6
|
import * as dom from "@src/layout/dom";
|
|
6
7
|
import * as mutation from "@src/layout/mutation";
|
|
7
8
|
|
|
@@ -25,16 +26,18 @@ export function link(node: Node): HTMLAnchorElement {
|
|
|
25
26
|
return null;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export function metadata(node: Node, event: Event): TargetMetadata {
|
|
29
|
+
export function metadata(node: Node, event: Event, text: string = null): TargetMetadata {
|
|
29
30
|
// If the node is null, we return a reserved value for id: 0. Valid assignment of id begins from 1+.
|
|
30
31
|
let output: TargetMetadata = { id: 0, hash: null, privacy: Privacy.Text, node };
|
|
31
32
|
if (node) {
|
|
32
33
|
let value = dom.get(node);
|
|
33
34
|
if (value !== null) {
|
|
35
|
+
let metadata = value.metadata;
|
|
34
36
|
output.id = value.id;
|
|
35
37
|
output.hash = value.hash;
|
|
36
|
-
output.privacy =
|
|
37
|
-
if (value.region) { track(value.region, event); }
|
|
38
|
+
output.privacy = metadata.privacy;
|
|
39
|
+
if (value.region) { region.track(value.region, event); }
|
|
40
|
+
if (metadata.fraud) { fraud.check(metadata.fraud, value.id, text || value.data.value); }
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|