clarity-js 0.6.42 → 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/build/clarity.js +832 -737
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +832 -737
- package/package.json +1 -1
- package/src/clarity.ts +1 -0
- package/src/core/config.ts +3 -1
- package/src/core/hash.ts +2 -2
- package/src/core/scrub.ts +26 -2
- package/src/core/time.ts +2 -2
- package/src/core/version.ts +1 -1
- package/src/data/metadata.ts +28 -31
- package/src/diagnostic/encode.ts +3 -2
- package/src/diagnostic/fraud.ts +13 -6
- package/src/interaction/change.ts +37 -0
- package/src/interaction/click.ts +1 -1
- package/src/interaction/clipboard.ts +1 -1
- package/src/interaction/encode.ts +22 -6
- package/src/interaction/index.ts +4 -0
- package/src/interaction/input.ts +2 -2
- package/src/interaction/pointer.ts +2 -2
- package/src/interaction/scroll.ts +1 -1
- package/src/interaction/submit.ts +1 -1
- package/src/interaction/unload.ts +2 -1
- package/src/interaction/visibility.ts +3 -2
- package/src/layout/dom.ts +22 -18
- package/src/layout/encode.ts +3 -3
- package/src/layout/index.ts +2 -0
- package/src/layout/node.ts +12 -1
- package/src/performance/observer.ts +5 -0
- package/test/core.test.ts +26 -14
- package/test/helper.ts +18 -1
- package/test/html/core.html +2 -0
- package/types/core.d.ts +4 -2
- package/types/data.d.ts +19 -4
- package/types/diagnostic.d.ts +1 -1
- package/types/interaction.d.ts +14 -0
- package/types/layout.d.ts +3 -2
package/package.json
CHANGED
package/src/clarity.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as layout from "@src/layout";
|
|
|
11
11
|
import * as performance from "@src/performance";
|
|
12
12
|
export { version };
|
|
13
13
|
export { consent, event, identify, set, upgrade, metadata } from "@src/data";
|
|
14
|
+
export { hashText } from "@src/layout";
|
|
14
15
|
|
|
15
16
|
const modules: Module[] = [diagnostic, layout, interaction, performance];
|
|
16
17
|
|
package/src/core/config.ts
CHANGED
|
@@ -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
|
|
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(
|
|
8
|
-
ts =
|
|
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
|
|
package/src/core/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let version = "0.
|
|
1
|
+
let version = "0.7.0";
|
|
2
2
|
export default version;
|
package/src/data/metadata.ts
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
package/src/diagnostic/encode.ts
CHANGED
|
@@ -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.
|
|
35
|
+
tokens.push(fraud.data.checksum);
|
|
35
36
|
queue(tokens, false);
|
|
36
37
|
}
|
|
37
38
|
break;
|
package/src/diagnostic/fraud.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { BooleanFlag, Event, Metric, Setting } from "@clarity-types/data";
|
|
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";
|
|
@@ -10,15 +11,21 @@ export let data: FraudData;
|
|
|
10
11
|
export function start(): void {
|
|
11
12
|
history = [];
|
|
12
13
|
metric.max(Metric.Automation, navigator.webdriver ? BooleanFlag.True : BooleanFlag.False);
|
|
14
|
+
try {
|
|
15
|
+
metric.max(Metric.Iframed, window.top == window.self ? IframeStatus.TopFrame : IframeStatus.Iframe);
|
|
16
|
+
} catch (ex) {
|
|
17
|
+
metric.max(Metric.Iframed, IframeStatus.Unknown);
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
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,
|
|
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) };
|
|
19
26
|
// Only encode this event if we haven't already reported this hash
|
|
20
|
-
if (history.indexOf(data.
|
|
21
|
-
history.push(data.
|
|
27
|
+
if (history.indexOf(data.checksum) < 0) {
|
|
28
|
+
history.push(data.checksum);
|
|
22
29
|
encode(Event.Fraud);
|
|
23
30
|
}
|
|
24
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
|
+
}
|
package/src/interaction/click.ts
CHANGED
|
@@ -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];
|
package/src/interaction/index.ts
CHANGED
|
@@ -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
|
}
|
package/src/interaction/input.ts
CHANGED
|
@@ -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.
|
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Privacy } from "@clarity-types/core";
|
|
2
|
-
import { Code, Severity } from "@clarity-types/data";
|
|
2
|
+
import { Code, Setting, Severity } from "@clarity-types/data";
|
|
3
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";
|
|
@@ -17,8 +17,9 @@ let override = [];
|
|
|
17
17
|
let unmask = [];
|
|
18
18
|
let updatedFragments: { [fragment: number]: string } = {};
|
|
19
19
|
let maskText = [];
|
|
20
|
-
let
|
|
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
|
-
|
|
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.
|
|
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);
|
|
@@ -307,6 +305,12 @@ function updateSelector(value: NodeValue): void {
|
|
|
307
305
|
}
|
|
308
306
|
}
|
|
309
307
|
|
|
308
|
+
export function hashText(hash: string): string {
|
|
309
|
+
let id = lookup(hash);
|
|
310
|
+
let node = getNode(id);
|
|
311
|
+
return node !== null && node.textContent !== null ? node.textContent.substr(0, Setting.ClickText) : '';
|
|
312
|
+
}
|
|
313
|
+
|
|
310
314
|
export function getNode(id: number): Node {
|
|
311
315
|
if (id in nodes) {
|
|
312
316
|
return nodes[id];
|
package/src/layout/encode.ts
CHANGED
|
@@ -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
|
}
|