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/src/layout/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import * as dom from "@src/layout/dom";
|
|
|
4
4
|
import * as mutation from "@src/layout/mutation";
|
|
5
5
|
import * as region from "@src/layout/region";
|
|
6
6
|
|
|
7
|
+
export { hashText } from "@src/layout/dom";
|
|
8
|
+
|
|
7
9
|
export function start(): void {
|
|
8
10
|
// The order below is important
|
|
9
11
|
// and is determined by interdependencies of modules
|
package/src/layout/node.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/test/core.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assert } from 'chai';
|
|
2
2
|
import { Browser, Page } from 'playwright';
|
|
3
|
-
import { clicks, inputs, launch, markup, node, text } from './helper';
|
|
4
|
-
import {
|
|
3
|
+
import { changes, clicks, inputs, launch, markup, node, text } from './helper';
|
|
4
|
+
import { decode } from "clarity-decode";
|
|
5
5
|
|
|
6
6
|
let browser: Browser;
|
|
7
7
|
let page: Page;
|
|
@@ -34,8 +34,10 @@ describe('Core Tests', () => {
|
|
|
34
34
|
let password = node(decoded, "attributes.id", "pwd");
|
|
35
35
|
let search = node(decoded, "attributes.id", "search");
|
|
36
36
|
let card = node(decoded, "attributes.id", "cardnum");
|
|
37
|
+
let textarea = text(decoded, "textarea");
|
|
37
38
|
let click = clicks(decoded)[0];
|
|
38
39
|
let input = inputs(decoded)[0];
|
|
40
|
+
let group = changes(decoded);
|
|
39
41
|
|
|
40
42
|
// Non-sensitive fields continue to pass through with sensitive bits masked off
|
|
41
43
|
assert.equal(heading, "Thanks for your order #▫▪▪▫▫▫▪▪");
|
|
@@ -43,13 +45,23 @@ describe('Core Tests', () => {
|
|
|
43
45
|
// Sensitive fields, including input fields, are randomized and masked
|
|
44
46
|
assert.equal(address, "•••••• ••••• ••••• ••••• ••••• •••••");
|
|
45
47
|
assert.equal(email.attributes.value, "••••• •••• •••• ••••");
|
|
46
|
-
assert.equal(password.attributes.value, "
|
|
47
|
-
assert.equal(search.attributes.value, "
|
|
48
|
-
assert.equal(card.attributes.value, "
|
|
48
|
+
assert.equal(password.attributes.value, "••••");
|
|
49
|
+
assert.equal(search.attributes.value, "••••• •••• ••••");
|
|
50
|
+
assert.equal(card.attributes.value, "•••••");
|
|
51
|
+
assert.equal(textarea, "••••• •••••");
|
|
49
52
|
|
|
50
53
|
// Clicked text and input value should be consistent with uber masking configuration
|
|
51
54
|
assert.equal(click.data.text, "Hello ▪▪▪▫▪");
|
|
52
|
-
assert.equal(input.data.value, "
|
|
55
|
+
assert.equal(input.data.value, "••••• •••• •••• ••••");
|
|
56
|
+
assert.equal(group.length, 2);
|
|
57
|
+
// Search change - we should captured mangled input and hash
|
|
58
|
+
assert.equal(group[0].data.type, "search");
|
|
59
|
+
assert.equal(group[0].data.value, "••••• •••• •••• ••••");
|
|
60
|
+
assert.equal(group[0].data.checksum, "4y7m6");
|
|
61
|
+
// Password change - we should capture placholder value and empty hash
|
|
62
|
+
assert.equal(group[1].data.type, "password");
|
|
63
|
+
assert.equal(group[1].data.value, "••••");
|
|
64
|
+
assert.equal(group[1].data.checksum, "");
|
|
53
65
|
});
|
|
54
66
|
|
|
55
67
|
it('should mask all text in strict mode', async () => {
|
|
@@ -68,7 +80,7 @@ describe('Core Tests', () => {
|
|
|
68
80
|
assert.equal(heading, "• ••••• ••••• ••••• ••••• •••••");
|
|
69
81
|
assert.equal(address, "•••••• ••••• ••••• ••••• ••••• •••••");
|
|
70
82
|
assert.equal(email.attributes.value, "••••• •••• •••• ••••");
|
|
71
|
-
assert.equal(password.attributes.value, "
|
|
83
|
+
assert.equal(password.attributes.value, "••••");
|
|
72
84
|
assert.equal(search.attributes.value, "••••• •••• ••••");
|
|
73
85
|
assert.equal(card.attributes.value, "•••••");
|
|
74
86
|
|
|
@@ -89,19 +101,19 @@ describe('Core Tests', () => {
|
|
|
89
101
|
let click = clicks(decoded)[0];
|
|
90
102
|
let input = inputs(decoded)[0];
|
|
91
103
|
|
|
92
|
-
// Text flows through unmasked for non-sensitive fields,
|
|
104
|
+
// Text flows through unmasked for non-sensitive fields, with exception of input fields
|
|
93
105
|
assert.equal(heading, "Thanks for your order #2AB700GH");
|
|
94
106
|
assert.equal(address, "1 Microsoft Way, Redmond, WA - 98052");
|
|
95
|
-
assert.equal(search.attributes.value, "
|
|
107
|
+
assert.equal(search.attributes.value, "••••• •••• ••••");
|
|
96
108
|
|
|
97
109
|
// Sensitive fields are still masked
|
|
98
110
|
assert.equal(email.attributes.value, "••••• •••• •••• ••••");
|
|
99
|
-
assert.equal(password.attributes.value, "
|
|
111
|
+
assert.equal(password.attributes.value, "••••");
|
|
100
112
|
assert.equal(card.attributes.value, "•••••");
|
|
101
113
|
|
|
102
|
-
// Clicked text
|
|
114
|
+
// Clicked text comes through unmasked in relaxed mode but input is still masked
|
|
103
115
|
assert.equal(click.data.text, "Hello Wor1d");
|
|
104
|
-
assert.equal(input.data.value, "
|
|
116
|
+
assert.equal(input.data.value, "••••• •••• •••• ••••");
|
|
105
117
|
});
|
|
106
118
|
|
|
107
119
|
it('should respect mask config even in relaxed mode', async () => {
|
|
@@ -114,8 +126,8 @@ describe('Core Tests', () => {
|
|
|
114
126
|
// Masked sub-trees continue to stay masked
|
|
115
127
|
assert.equal(subtree, "••••• •••••");
|
|
116
128
|
|
|
117
|
-
// Clicked text is masked due to masked configuration
|
|
129
|
+
// Clicked text is masked due to masked configuration and input value is also masked
|
|
118
130
|
assert.equal(click.data.text, "••••• •••• ••••");
|
|
119
|
-
assert.equal(input.data.value, "
|
|
131
|
+
assert.equal(input.data.value, "••••• •••• •••• ••••");
|
|
120
132
|
});
|
|
121
133
|
});
|
package/test/helper.ts
CHANGED
|
@@ -26,7 +26,11 @@ export async function markup(page: Page, file: string, override: Core.Config = n
|
|
|
26
26
|
`));
|
|
27
27
|
await page.hover("#two");
|
|
28
28
|
await page.click("#child");
|
|
29
|
-
await page.locator('#search').fill('
|
|
29
|
+
await page.locator('#search').fill('');
|
|
30
|
+
await page.locator('#search').type('query with numb3rs');
|
|
31
|
+
await page.locator('#pwd').type('p1ssw0rd');
|
|
32
|
+
await page.locator('#eml').fill('');
|
|
33
|
+
await page.locator('#eml').type('hello@world.com');
|
|
30
34
|
await page.waitForFunction("payloads && payloads.length > 2");
|
|
31
35
|
return await page.evaluate('payloads');
|
|
32
36
|
}
|
|
@@ -57,6 +61,19 @@ export function inputs(decoded: Data.DecodedPayload[]): Interaction.InputEvent[]
|
|
|
57
61
|
return output;
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
export function changes(decoded: Data.DecodedPayload[]): Interaction.ChangeEvent[] {
|
|
65
|
+
let output: Interaction.ChangeEvent[] = [];
|
|
66
|
+
for (let i = decoded.length - 1; i >= 0; i--) {
|
|
67
|
+
if (decoded[i].change) {
|
|
68
|
+
for (let j = 0; j < decoded[i].change.length;j++)
|
|
69
|
+
{
|
|
70
|
+
output.push(decoded[i].change[j]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return output;
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
export function node(decoded: Data.DecodedPayload[], key: string, value: string | number, tag: string = null): Layout.DomData {
|
|
61
78
|
let sub = null;
|
|
62
79
|
|
package/test/html/core.html
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<title>Core Tests</title>
|
|
5
|
+
<style>input, textarea, select { margin: 10px; display: block; }</style>
|
|
5
6
|
</head>
|
|
6
7
|
<body>
|
|
7
8
|
<div>
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
<input type="password" id="pwd" title="Password" maxlength="16" value="passw0rd">
|
|
17
18
|
<input type="search" id="search" title="Search" value="hello w0rld">
|
|
18
19
|
<input type="text" id="cardnum" title="CC" value="1234">
|
|
20
|
+
<textarea id="textarea" autocapitalize="off" role="combobox" rows="5" placeholder="" spellcheck="false">Hell0 World</textarea>
|
|
19
21
|
</form>
|
|
20
22
|
</body>
|
|
21
23
|
</html>
|
package/types/core.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ type TaskFunction = () => Promise<void>;
|
|
|
4
4
|
type TaskResolve = () => void;
|
|
5
5
|
type UploadCallback = (data: string) => void;
|
|
6
6
|
type Region = [number /* RegionId */, string /* Query Selector */];
|
|
7
|
-
type
|
|
7
|
+
type Checksum = [number /* FraudId */, string /* Query Selector */];
|
|
8
8
|
export type Extract = ExtractSource /* Extraction Source */ | number /* Extract Id */ | string | string[] /* Hash or Query Selector or String Token */;
|
|
9
9
|
|
|
10
10
|
/* Enum */
|
|
@@ -123,12 +123,14 @@ export interface Config {
|
|
|
123
123
|
lean?: boolean;
|
|
124
124
|
track?: boolean;
|
|
125
125
|
content?: boolean;
|
|
126
|
+
drop?: string[];
|
|
126
127
|
mask?: string[];
|
|
127
128
|
unmask?: string[];
|
|
128
129
|
regions?: Region[];
|
|
129
130
|
extract?: Extract[];
|
|
130
131
|
cookies?: string[];
|
|
131
|
-
fraud?:
|
|
132
|
+
fraud?: boolean;
|
|
133
|
+
checksum?: Checksum[];
|
|
132
134
|
report?: string;
|
|
133
135
|
upload?: string | UploadCallback;
|
|
134
136
|
fallback?: string;
|
package/types/data.d.ts
CHANGED
|
@@ -62,7 +62,8 @@ export const enum Event {
|
|
|
62
62
|
Clipboard = 38,
|
|
63
63
|
Submit = 39,
|
|
64
64
|
Extract = 40,
|
|
65
|
-
Fraud = 41
|
|
65
|
+
Fraud = 41,
|
|
66
|
+
Change = 42
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export const enum Metric {
|
|
@@ -96,7 +97,11 @@ export const enum Metric {
|
|
|
96
97
|
Mobile = 27,
|
|
97
98
|
UploadTime = 28,
|
|
98
99
|
SinglePage = 29,
|
|
99
|
-
UsedMemory = 30
|
|
100
|
+
UsedMemory = 30,
|
|
101
|
+
Iframed = 31,
|
|
102
|
+
MaxTouchPoints = 32,
|
|
103
|
+
HardwareConcurrency = 33,
|
|
104
|
+
DeviceMemory = 34
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
export const enum Dimension {
|
|
@@ -125,7 +130,9 @@ export const enum Dimension {
|
|
|
125
130
|
Platform = 22,
|
|
126
131
|
PlatformVersion = 23,
|
|
127
132
|
Brand = 24,
|
|
128
|
-
Model = 25
|
|
133
|
+
Model = 25,
|
|
134
|
+
DevicePixelRatio = 26,
|
|
135
|
+
ConnectionType = 27
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
export const enum Check {
|
|
@@ -170,6 +177,12 @@ export const enum BooleanFlag {
|
|
|
170
177
|
True = 1
|
|
171
178
|
}
|
|
172
179
|
|
|
180
|
+
export const enum IframeStatus {
|
|
181
|
+
Unknown = 0,
|
|
182
|
+
TopFrame = 1,
|
|
183
|
+
Iframe = 2
|
|
184
|
+
}
|
|
185
|
+
|
|
173
186
|
export const enum Setting {
|
|
174
187
|
Expire = 365, // 1 Year
|
|
175
188
|
SessionExpire = 1, // 1 Day
|
|
@@ -200,7 +213,8 @@ export const enum Setting {
|
|
|
200
213
|
UploadFactor = 3, // Slow down sequence by specified factor
|
|
201
214
|
MinUploadDelay = 100, // Minimum time before we are ready to flush events to the server
|
|
202
215
|
MaxUploadDelay = 30 * Time.Second, // Do flush out payload once every 30s,
|
|
203
|
-
ExtractLimit = 10000 // Do not extract more than 10000 characters
|
|
216
|
+
ExtractLimit = 10000, // Do not extract more than 10000 characters
|
|
217
|
+
ChecksumPrecision = 24 // n-bit integer to represent token hash
|
|
204
218
|
}
|
|
205
219
|
|
|
206
220
|
export const enum Character {
|
|
@@ -227,6 +241,7 @@ export const enum Constant {
|
|
|
227
241
|
Space = " ",
|
|
228
242
|
Expires = "expires=",
|
|
229
243
|
Domain = "domain=",
|
|
244
|
+
Dropped = "*na*",
|
|
230
245
|
Comma = ",",
|
|
231
246
|
Dot = ".",
|
|
232
247
|
Semicolon = ";",
|
package/types/diagnostic.d.ts
CHANGED
package/types/interaction.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export const enum BrowsingContext {
|
|
|
12
12
|
|
|
13
13
|
export const enum Setting {
|
|
14
14
|
LookAhead = 500, // 500ms
|
|
15
|
+
InputLookAhead = 1000, // 1s
|
|
15
16
|
Distance = 20, // 20 pixels
|
|
16
17
|
Interval = 25, // 25 milliseconds
|
|
17
18
|
TimelineSpan = 2 * Time.Second, // 2 seconds
|
|
@@ -54,6 +55,12 @@ export interface SubmitState {
|
|
|
54
55
|
data: SubmitData;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
export interface ChangeState {
|
|
59
|
+
time: number;
|
|
60
|
+
event: number;
|
|
61
|
+
data: ChangeData;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
export interface InputState {
|
|
58
65
|
time: number;
|
|
59
66
|
event: number;
|
|
@@ -76,6 +83,13 @@ export interface TimelineData {
|
|
|
76
83
|
context: number;
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
export interface ChangeData {
|
|
87
|
+
target: Target;
|
|
88
|
+
type: string;
|
|
89
|
+
value: string;
|
|
90
|
+
checksum: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
export interface InputData {
|
|
80
94
|
target: Target;
|
|
81
95
|
value: string;
|
package/types/layout.d.ts
CHANGED
|
@@ -30,8 +30,9 @@ export const enum RegionVisibility {
|
|
|
30
30
|
|
|
31
31
|
export const enum Mask {
|
|
32
32
|
Text = "address,password,contact",
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
Disable = "radio,checkbox,range,button,reset,submit",
|
|
34
|
+
Exclude = "password,secret,pass,social,ssn,code,hidden",
|
|
35
|
+
Tags = "INPUT,SELECT,TEXTAREA"
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export const enum Constant {
|