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.
@@ -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
@@ -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.
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 { Data, decode } from "clarity-decode";
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, "hello ▪▫▪▪▪");
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, "query with ▪▪▪▪▫▪▪");
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, including input 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, "hello w0rld");
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 and input value (non-sensitive) both come through without masking in relaxed mode
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, "query with numb3rs");
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 while input value is not masked in relaxed mode
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, "query with numb3rs");
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('query with numb3rs');
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
 
@@ -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 Fraud = [number /* FraudId */, string /* Query Selector */];
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?: 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 = ";",
@@ -20,5 +20,5 @@ export interface LogData {
20
20
  export interface FraudData {
21
21
  id: number;
22
22
  target: number;
23
- hash: string;
23
+ checksum: string;
24
24
  }
@@ -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
- Input = "password,secret,pass,social,ssn,name,code,dob,cell,mob,contact,hidden,account,cvv,ccv,email,tel,phone,address,addr,card,zip",
34
- Disable = "radio,checkbox,range,button,reset,submit"
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 {