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/build/clarity.js +361 -281
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +361 -281
- package/package.json +1 -1
- 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 +6 -5
- 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 +15 -17
- package/src/layout/encode.ts +3 -3
- 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 +11 -3
- package/types/diagnostic.d.ts +1 -1
- package/types/interaction.d.ts +14 -0
- package/types/layout.d.ts +3 -2
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 {
|
|
@@ -98,6 +99,9 @@ export const enum Metric {
|
|
|
98
99
|
SinglePage = 29,
|
|
99
100
|
UsedMemory = 30,
|
|
100
101
|
Iframed = 31,
|
|
102
|
+
MaxTouchPoints = 32,
|
|
103
|
+
HardwareConcurrency = 33,
|
|
104
|
+
DeviceMemory = 34
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
export const enum Dimension {
|
|
@@ -126,7 +130,9 @@ export const enum Dimension {
|
|
|
126
130
|
Platform = 22,
|
|
127
131
|
PlatformVersion = 23,
|
|
128
132
|
Brand = 24,
|
|
129
|
-
Model = 25
|
|
133
|
+
Model = 25,
|
|
134
|
+
DevicePixelRatio = 26,
|
|
135
|
+
ConnectionType = 27
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
export const enum Check {
|
|
@@ -207,7 +213,8 @@ export const enum Setting {
|
|
|
207
213
|
UploadFactor = 3, // Slow down sequence by specified factor
|
|
208
214
|
MinUploadDelay = 100, // Minimum time before we are ready to flush events to the server
|
|
209
215
|
MaxUploadDelay = 30 * Time.Second, // Do flush out payload once every 30s,
|
|
210
|
-
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
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
export const enum Character {
|
|
@@ -234,6 +241,7 @@ export const enum Constant {
|
|
|
234
241
|
Space = " ",
|
|
235
242
|
Expires = "expires=",
|
|
236
243
|
Domain = "domain=",
|
|
244
|
+
Dropped = "*na*",
|
|
237
245
|
Comma = ",",
|
|
238
246
|
Dot = ".",
|
|
239
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 {
|