clarity-js 0.6.33 → 0.6.36

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.
@@ -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 box from "./box";
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 size = value.metadata.size;
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 (size && size.length === 2) { tokens.push(`${Constant.Box}${str(size[0])}.${str(size[1])}`); }
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
  }
@@ -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
  }
@@ -2,6 +2,7 @@ import { Priority, Task, Timer } from "@clarity-types/core";
2
2
  import { Code, Event, Metric, Severity } from "@clarity-types/data";
3
3
  import { Constant, MutationHistory, MutationQueue, Setting, Source } from "@clarity-types/layout";
4
4
  import api from "@src/core/api";
5
+ import * as core from "@src/core";
5
6
  import { bind } from "@src/core/event";
6
7
  import measure from "@src/core/measure";
7
8
  import * as task from "@src/core/task";
@@ -35,32 +36,38 @@ export function start(): void {
35
36
  activePeriod = 0;
36
37
  history = {};
37
38
 
38
- if (insertRule === null) { insertRule = CSSStyleSheet.prototype.insertRule; }
39
- if (deleteRule === null) { deleteRule = CSSStyleSheet.prototype.deleteRule; }
40
- if (attachShadow === null) { attachShadow = Element.prototype.attachShadow; }
41
-
42
39
  // Some popular open source libraries, like styled-components, optimize performance
43
40
  // by injecting CSS using insertRule API vs. appending text node. A side effect of
44
41
  // using javascript API is that it doesn't trigger DOM mutation and therefore we
45
42
  // need to override the insertRule API and listen for changes manually.
46
- CSSStyleSheet.prototype.insertRule = function(): number {
47
- schedule(this.ownerNode);
48
- return insertRule.apply(this, arguments);
49
- };
50
-
51
- CSSStyleSheet.prototype.deleteRule = function(): void {
52
- schedule(this.ownerNode);
53
- return deleteRule.apply(this, arguments);
54
- };
43
+ if (insertRule === null) {
44
+ insertRule = CSSStyleSheet.prototype.insertRule;
45
+ CSSStyleSheet.prototype.insertRule = function(): number {
46
+ if (core.active()) { schedule(this.ownerNode); }
47
+ return insertRule.apply(this, arguments);
48
+ };
49
+ }
55
50
 
56
- // Add a hook to attachShadow API calls
57
- // In case we are unable to add a hook and browser throws an exception,
58
- // reset attachShadow variable and resume processing like before
59
- try {
60
- Element.prototype.attachShadow = function (): ShadowRoot {
61
- return schedule(attachShadow.apply(this, arguments)) as ShadowRoot;
62
- }
63
- } catch { attachShadow = null; }
51
+ if (deleteRule === null) {
52
+ deleteRule = CSSStyleSheet.prototype.deleteRule;
53
+ CSSStyleSheet.prototype.deleteRule = function(): void {
54
+ if (core.active()) { schedule(this.ownerNode); }
55
+ return deleteRule.apply(this, arguments);
56
+ };
57
+ }
58
+
59
+ // Add a hook to attachShadow API calls
60
+ // In case we are unable to add a hook and browser throws an exception,
61
+ // reset attachShadow variable and resume processing like before
62
+ if (attachShadow === null) {
63
+ attachShadow = Element.prototype.attachShadow;
64
+ try {
65
+ Element.prototype.attachShadow = function (): ShadowRoot {
66
+ if (core.active()) { return schedule(attachShadow.apply(this, arguments)) as ShadowRoot; }
67
+ else { return attachShadow.apply(this, arguments)}
68
+ }
69
+ } catch { attachShadow = null; }
70
+ }
64
71
  }
65
72
 
66
73
  export function observe(node: Node): void {
@@ -90,25 +97,6 @@ export function monitor(frame: HTMLIFrameElement): void {
90
97
  export function stop(): void {
91
98
  for (let observer of observers) { if (observer) { observer.disconnect(); } }
92
99
  observers = [];
93
-
94
- // Restoring original insertRule
95
- if (insertRule !== null) {
96
- CSSStyleSheet.prototype.insertRule = insertRule;
97
- insertRule = null;
98
- }
99
-
100
- // Restoring original deleteRule
101
- if (deleteRule !== null) {
102
- CSSStyleSheet.prototype.deleteRule = deleteRule;
103
- deleteRule = null;
104
- }
105
-
106
- // Restoring original attachShadow
107
- if (attachShadow != null) {
108
- Element.prototype.attachShadow = attachShadow;
109
- attachShadow = null;
110
- }
111
-
112
100
  history = {};
113
101
  mutations = [];
114
102
  queue = [];
@@ -126,7 +114,7 @@ function handle(m: MutationRecord[]): void {
126
114
  summary.track(Event.Mutation, now);
127
115
  mutations.push({ time: now, mutations: m});
128
116
  task.schedule(process, Priority.High).then((): void => {
129
- measure(doc.compute)();
117
+ setTimeout(doc.compute)
130
118
  measure(region.compute)();
131
119
  });
132
120
  }
@@ -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 {
@@ -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 { track } from "@src/layout/region";
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 = value.metadata.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
 
@@ -1,4 +1,5 @@
1
- import { Code, Constant, Dimension, Metric, Severity } from "@clarity-types/data";
1
+ import { Code, Constant, Dimension, Metric, Setting, Severity } from "@clarity-types/data";
2
+ import config from "@src/core/config";
2
3
  import { bind } from "@src/core/event";
3
4
  import measure from "@src/core/measure";
4
5
  import { setTimeout } from "@src/core/timeout";
@@ -56,7 +57,9 @@ function process(entries: PerformanceEntryList): void {
56
57
  navigation.compute(entry as PerformanceNavigationTiming);
57
58
  break;
58
59
  case Constant.Resource:
59
- dimension.log(Dimension.NetworkHosts, host(entry.name));
60
+ let name = entry.name;
61
+ dimension.log(Dimension.NetworkHosts, host(name));
62
+ if (name === config.upload || name === config.fallback) { metric.max(Metric.UploadTime, entry.duration); }
60
63
  break;
61
64
  case Constant.LongTask:
62
65
  metric.count(Metric.LongTaskCount);
@@ -73,6 +76,11 @@ function process(entries: PerformanceEntryList): void {
73
76
  break;
74
77
  }
75
78
  }
79
+ if (performance && Constant.Memory in performance && performance[Constant.Memory].usedJSHeapSize) {
80
+ // Track consumed memory (MBs) where "memory" API is available
81
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory
82
+ metric.max(Metric.UsedMemory, Math.abs(performance[Constant.Memory].usedJSHeapSize / Setting.MegaByte));
83
+ }
76
84
  }
77
85
 
78
86
  export function stop(): void {
package/types/core.d.ts CHANGED
@@ -4,6 +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
8
  export type Extract = ExtractSource /* Extraction Source */ | number /* Extract Id */ | string | string[] /* Hash or Query Selector or String Token */;
8
9
 
9
10
  /* Enum */
@@ -127,6 +128,7 @@ export interface Config {
127
128
  regions?: Region[];
128
129
  extract?: Extract[];
129
130
  cookies?: string[];
131
+ fraud?: Fraud[];
130
132
  report?: string;
131
133
  upload?: string | UploadCallback;
132
134
  fallback?: string;
package/types/data.d.ts CHANGED
@@ -4,9 +4,12 @@ export type Token = (string | number | number[] | string[]);
4
4
  export type DecodedToken = (any | any[]);
5
5
 
6
6
  export type MetadataCallback = (data: Metadata, playback: boolean) => void;
7
+ export interface MetadataCallbackOptions {
8
+ callback: MetadataCallback,
9
+ wait: boolean
10
+ }
7
11
 
8
12
  /* Enum */
9
-
10
13
  export const enum Event {
11
14
  /* Data */
12
15
  Metric = 0,
@@ -52,10 +55,14 @@ export const enum Event {
52
55
  Variable = 34,
53
56
  Limit = 35,
54
57
  Summary = 36,
58
+ /**
59
+ * @deprecated No longer support Box event
60
+ */
55
61
  Box = 37,
56
62
  Clipboard = 38,
57
63
  Submit = 39,
58
- Extract = 40
64
+ Extract = 40,
65
+ Fraud = 41
59
66
  }
60
67
 
61
68
  export const enum Metric {
@@ -86,7 +93,10 @@ export const enum Metric {
86
93
  CartTotal = 24,
87
94
  EventCount = 25,
88
95
  Automation = 26,
89
- Mobile = 27
96
+ Mobile = 27,
97
+ UploadTime = 28,
98
+ SinglePage = 29,
99
+ UsedMemory = 30
90
100
  }
91
101
 
92
102
  export const enum Dimension {
@@ -177,7 +187,6 @@ export const enum Setting {
177
187
  CollectionLimit = 128, // Number of unique entries for dimensions
178
188
  ClickPrecision = 32767, // 2^15 - 1
179
189
  BoxPrecision = 100, // Up to 2 decimal points (e.g. 34.56)
180
- ResizeObserverThreshold = 15, // At least 15 characters before we attach a resize observer for the node
181
190
  ScriptErrorLimit = 5, // Do not send the same script error more than 5 times per page
182
191
  DimensionLimit = 256, // Do not extract dimensions which are over 256 characters
183
192
  WordLength = 5, // Estimated average size of a word,
@@ -187,6 +196,7 @@ export const enum Setting {
187
196
  ViewportIntersectionRatio = 0.05, // Ratio of intersection area in comparison to viewport area before it's marked visible
188
197
  IntersectionRatio = 0.8, // Ratio of intersection area in comparison to element's area before it's marked visible
189
198
  MaxFirstPayloadBytes = 1 * 1024 * 1024, // 1MB: Cap the very first payload to a maximum of 1MB
199
+ MegaByte = 1024 * 1024, // 1MB
190
200
  UploadFactor = 3, // Slow down sequence by specified factor
191
201
  MinUploadDelay = 100, // Minimum time before we are ready to flush events to the server
192
202
  MaxUploadDelay = 30 * Time.Second, // Do flush out payload once every 30s,
@@ -212,6 +222,7 @@ export const enum Constant {
212
222
  Pause = "pause",
213
223
  Resume = "resume",
214
224
  Report = "report",
225
+ Memory = "memory",
215
226
  Empty = "",
216
227
  Space = " ",
217
228
  Expires = "expires=",
@@ -236,7 +247,6 @@ export const enum Constant {
236
247
  UserId = "userId",
237
248
  SessionId = "sessionId",
238
249
  PageId = "pageId",
239
- ResizeObserver = "ResizeObserver",
240
250
  Mask = "•",
241
251
  SessionStorage = "sessionStorage",
242
252
  Cookie = "cookie",
@@ -16,3 +16,9 @@ export interface LogData {
16
16
  stack: string;
17
17
  severity: Severity;
18
18
  }
19
+
20
+ export interface FraudData {
21
+ id: number;
22
+ target: number;
23
+ hash: string;
24
+ }
@@ -103,6 +103,7 @@ export interface ClickData {
103
103
  text: string;
104
104
  link: string;
105
105
  hash: string;
106
+ trust: number;
106
107
  }
107
108
 
108
109
  export interface ClipboardData {
package/types/layout.d.ts CHANGED
@@ -28,6 +28,11 @@ export const enum RegionVisibility {
28
28
  ScrolledToEnd = 13
29
29
  }
30
30
 
31
+ export const enum Mask {
32
+ Text = "password,secret,pass,social,ssn,name,code,dob,cell,mob,contact,hidden,account,cvv,ccv,email,tel,phone,address,addr,card,zip",
33
+ Disable = "radio,checkbox,range,button,reset,submit"
34
+ }
35
+
31
36
  export const enum Constant {
32
37
  Empty = "",
33
38
  SvgPrefix = "svg:",
@@ -42,6 +47,7 @@ export const enum Constant {
42
47
  Box = "#",
43
48
  Bang = "!",
44
49
  Period = ".",
50
+ Comma = ",",
45
51
  MaskData = "data-clarity-mask",
46
52
  UnmaskData = "data-clarity-unmask",
47
53
  RegionData = "data-clarity-region",
@@ -162,6 +168,7 @@ export interface NodeMeta {
162
168
  suspend: boolean;
163
169
  privacy: Privacy;
164
170
  position: number;
171
+ fraud: number;
165
172
  size: number[];
166
173
  }
167
174
 
@@ -198,12 +205,6 @@ export interface RegionData {
198
205
  name: string;
199
206
  }
200
207
 
201
- export interface BoxData {
202
- id: number;
203
- width: number;
204
- height: number;
205
- }
206
-
207
208
  export interface TargetMetadata {
208
209
  id: number;
209
210
  hash: [string, string];
@@ -31,10 +31,3 @@ export interface NavigationData {
31
31
  encodedSize: number;
32
32
  decodedSize: number;
33
33
  }
34
-
35
- export interface ConnectionData {
36
- downlink: number;
37
- rtt: number;
38
- saveData: BooleanFlag;
39
- type: string;
40
- }
package/src/layout/box.ts DELETED
@@ -1,83 +0,0 @@
1
- import { Event, Setting } from "@clarity-types/data";
2
- import { BoxData } from "@clarity-types/layout";
3
- import * as dom from "@src/layout/dom";
4
- import encode from "@src/layout/encode";
5
-
6
- export let data: BoxData[] = [];
7
- let enabled = false;
8
- let observer: ResizeObserver = null;
9
-
10
- export function start(): void {
11
- reset();
12
- observer = null;
13
- enabled = window["ResizeObserver"] ? true : false;
14
- }
15
-
16
- export function compute(id: number): void {
17
- if (enabled === false) { return; }
18
- observer = observer === null ? new ResizeObserver(handler) : observer;
19
- if (observer) {
20
- let value = dom.getValue(id);
21
- // If this is the first time computing size for this node, go ahead and wire up ResizeObserver
22
- // In all other cases, value.metadata.size will be null or an array with two elements [width, height]
23
- // And, in those cases, we will skip through the following section and not attach the observer
24
- if (value && value.metadata.size !== null && value.metadata.size.length === 0) {
25
- let node = dom.getNode(id);
26
- if (node && node.nodeType === Node.ELEMENT_NODE) {
27
- let e = node as HTMLElement;
28
- let r = e.getBoundingClientRect();
29
- value.metadata.size = [Math.floor(r.width * Setting.BoxPrecision), Math.floor(r.height * Setting.BoxPrecision)];
30
- observer.observe(e);
31
- }
32
- }
33
- }
34
- }
35
-
36
- function handler(entries: ResizeObserverEntry[]): void {
37
- window.requestAnimationFrame(() => {
38
- for (let entry of entries) {
39
- let target = entry.target;
40
- let id = target ? dom.getId(target) : null;
41
- if (id) {
42
- let v = dom.getValue(id);
43
- let s = v.metadata.size;
44
- let b = entry.borderBoxSize as any;
45
- let w = null;
46
- let h = null;
47
- // Check if browser supports borderBoxSize property on ResizeObserverEntry object
48
- // Otherwise, fall back to using getBoundingClientRect() to be cross browser compatible
49
- // Reference: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry/borderBoxSize
50
- if(b && b.length > 0) {
51
- w = Math.floor(b[0].inlineSize * Setting.BoxPrecision);
52
- h = Math.floor(b[0].blockSize * Setting.BoxPrecision);
53
- } else {
54
- let r = target.getBoundingClientRect();
55
- w = Math.floor(r.width * Setting.BoxPrecision);
56
- h = Math.floor(r.height * Setting.BoxPrecision);
57
- }
58
-
59
- // Capture new width & height only if they are different from what we have in in-memory cache
60
- if (w !== s[0] && h !== s[1]) {
61
- s = [w,h];
62
- data.push({ id, width: w, height: h });
63
- }
64
- }
65
- }
66
-
67
- // Schedule encode only when we have at least one valid data entry
68
- if (data.length > 0) { encode(Event.Box); }
69
- });
70
- }
71
-
72
- export function reset(): void {
73
- data = [];
74
- }
75
-
76
- export function stop(): void {
77
- reset();
78
- if (observer) {
79
- observer.disconnect();
80
- observer = null;
81
- }
82
- enabled = false;
83
- }