clarity-js 0.8.10-beta → 0.8.10

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.
Files changed (90) hide show
  1. package/.lintstagedrc.yml +3 -0
  2. package/biome.json +43 -0
  3. package/build/clarity.extended.js +1 -1
  4. package/build/clarity.insight.js +1 -1
  5. package/build/clarity.js +3581 -3019
  6. package/build/clarity.min.js +1 -1
  7. package/build/clarity.module.js +3581 -3019
  8. package/build/clarity.performance.js +1 -1
  9. package/package.json +17 -10
  10. package/rollup.config.ts +84 -88
  11. package/src/clarity.ts +34 -28
  12. package/src/core/config.ts +2 -2
  13. package/src/core/event.ts +36 -32
  14. package/src/core/hash.ts +5 -6
  15. package/src/core/history.ts +10 -11
  16. package/src/core/index.ts +21 -11
  17. package/src/core/measure.ts +9 -5
  18. package/src/core/report.ts +9 -5
  19. package/src/core/scrub.ts +29 -20
  20. package/src/core/task.ts +73 -45
  21. package/src/core/time.ts +3 -3
  22. package/src/core/timeout.ts +2 -2
  23. package/src/core/version.ts +1 -1
  24. package/src/data/baseline.ts +60 -55
  25. package/src/data/consent.ts +2 -2
  26. package/src/data/custom.ts +8 -13
  27. package/src/data/dimension.ts +11 -7
  28. package/src/data/encode.ts +36 -30
  29. package/src/data/envelope.ts +38 -38
  30. package/src/data/extract.ts +86 -77
  31. package/src/data/index.ts +10 -6
  32. package/src/data/limit.ts +1 -1
  33. package/src/data/metadata.ts +305 -266
  34. package/src/data/metric.ts +18 -8
  35. package/src/data/ping.ts +8 -4
  36. package/src/data/signal.ts +18 -18
  37. package/src/data/summary.ts +6 -4
  38. package/src/data/token.ts +10 -8
  39. package/src/data/upgrade.ts +7 -3
  40. package/src/data/upload.ts +100 -49
  41. package/src/data/variable.ts +27 -20
  42. package/src/diagnostic/encode.ts +2 -2
  43. package/src/diagnostic/fraud.ts +3 -4
  44. package/src/diagnostic/internal.ts +11 -5
  45. package/src/diagnostic/script.ts +12 -8
  46. package/src/global.ts +1 -1
  47. package/src/insight/blank.ts +4 -4
  48. package/src/insight/encode.ts +23 -17
  49. package/src/insight/snapshot.ts +57 -37
  50. package/src/interaction/change.ts +9 -6
  51. package/src/interaction/click.ts +34 -28
  52. package/src/interaction/clipboard.ts +2 -2
  53. package/src/interaction/encode.ts +35 -31
  54. package/src/interaction/input.ts +11 -9
  55. package/src/interaction/pointer.ts +41 -30
  56. package/src/interaction/resize.ts +5 -5
  57. package/src/interaction/scroll.ts +20 -17
  58. package/src/interaction/selection.ts +12 -8
  59. package/src/interaction/submit.ts +2 -2
  60. package/src/interaction/timeline.ts +13 -9
  61. package/src/interaction/unload.ts +1 -1
  62. package/src/interaction/visibility.ts +2 -2
  63. package/src/layout/animation.ts +47 -41
  64. package/src/layout/discover.ts +5 -5
  65. package/src/layout/document.ts +31 -19
  66. package/src/layout/dom.ts +141 -91
  67. package/src/layout/encode.ts +52 -37
  68. package/src/layout/mutation.ts +321 -318
  69. package/src/layout/node.ts +104 -81
  70. package/src/layout/offset.ts +7 -6
  71. package/src/layout/region.ts +66 -43
  72. package/src/layout/schema.ts +15 -8
  73. package/src/layout/selector.ts +47 -25
  74. package/src/layout/style.ts +44 -36
  75. package/src/layout/target.ts +14 -10
  76. package/src/layout/traverse.ts +17 -11
  77. package/src/performance/blank.ts +1 -1
  78. package/src/performance/encode.ts +4 -4
  79. package/src/performance/interaction.ts +58 -70
  80. package/src/performance/navigation.ts +2 -2
  81. package/src/performance/observer.ts +59 -26
  82. package/src/queue.ts +16 -9
  83. package/tsconfig.json +2 -2
  84. package/tslint.json +25 -32
  85. package/types/core.d.ts +13 -13
  86. package/types/data.d.ts +32 -29
  87. package/types/diagnostic.d.ts +1 -1
  88. package/types/interaction.d.ts +5 -4
  89. package/types/layout.d.ts +36 -21
  90. package/types/performance.d.ts +6 -5
@@ -1,5 +1,5 @@
1
1
  import { Character } from "../../types/data";
2
- import { Constant, Selector, SelectorInput } from "../../types/layout";
2
+ import { Constant, Selector, type SelectorInput } from "../../types/layout";
3
3
 
4
4
  const excludeClassNames = Constant.ExcludeClassNames.split(Constant.Comma);
5
5
  let selectorMap: { [selector: string]: number[] } = {};
@@ -9,9 +9,9 @@ export function reset(): void {
9
9
  }
10
10
 
11
11
  export function get(input: SelectorInput, type: Selector): string {
12
- let a = input.attributes;
12
+ const a = input.attributes;
13
13
  let prefix = input.prefix ? input.prefix[type] : null;
14
- let suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position-1}` : `:nth-of-type(${input.position})`;
14
+ const suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position - 1}` : `:nth-of-type(${input.position})`;
15
15
  switch (input.tag) {
16
16
  case "STYLE":
17
17
  case "TITLE":
@@ -22,24 +22,37 @@ export function get(input: SelectorInput, type: Selector): string {
22
22
  return Constant.Empty;
23
23
  case "HTML":
24
24
  return Constant.HTML;
25
- default:
26
- if (prefix === null) { return Constant.Empty; }
25
+ default: {
26
+ if (prefix === null) {
27
+ return Constant.Empty;
28
+ }
27
29
  prefix = `${prefix}${Constant.Separator}`;
28
30
  input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag;
29
31
  let selector = `${prefix}${input.tag}${suffix}`;
30
- let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
31
- let classes = input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/).filter(c => filter(c)).join(Constant.Period) : null;
32
+ const id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
33
+ const classes =
34
+ input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0
35
+ ? a[Constant.Class]
36
+ .trim()
37
+ .split(/\s+/)
38
+ .filter((c) => filter(c))
39
+ .join(Constant.Period)
40
+ : null;
32
41
  if (classes && classes.length > 0) {
33
42
  if (type === Selector.Alpha) {
34
43
  // In Alpha mode, update selector to use class names, with relative positioning within the parent id container.
35
44
  // If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple.
36
- let key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`;
37
- if (!(key in selectorMap)) { selectorMap[key] = []; }
38
- if (selectorMap[key].indexOf(input.id) < 0) { selectorMap[key].push(input.id); }
45
+ const key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`;
46
+ if (!(key in selectorMap)) {
47
+ selectorMap[key] = [];
48
+ }
49
+ if (selectorMap[key].indexOf(input.id) < 0) {
50
+ selectorMap[key].push(input.id);
51
+ }
39
52
  selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`;
40
53
  } else {
41
54
  // In Beta mode, we continue to look at query selectors in context of the full page
42
- selector = `${prefix}${input.tag}.${classes}${suffix}`
55
+ selector = `${prefix}${input.tag}.${classes}${suffix}`;
43
56
  }
44
57
  }
45
58
  // Update selector to use "id" field when available. There are two exceptions:
@@ -47,36 +60,45 @@ export function get(input: SelectorInput, type: Selector): string {
47
60
  // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
48
61
  selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector;
49
62
  return selector;
63
+ }
50
64
  }
51
65
  }
52
66
 
53
67
  function getDomPrefix(prefix: string): string {
54
- const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag);
55
- const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
56
- const domStart = Math.max(shadowDomStart, iframeDomStart);
57
-
58
- if (domStart < 0) { return Constant.Empty; }
68
+ const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag);
69
+ const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
70
+ const domStart = Math.max(shadowDomStart, iframeDomStart);
71
+
72
+ if (domStart < 0) {
73
+ return Constant.Empty;
74
+ }
59
75
 
60
- return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1);
76
+ return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1);
61
77
  }
62
78
 
63
79
  function getDomPath(input: string): string {
64
- let parts = input.split(Constant.Separator);
80
+ const parts = input.split(Constant.Separator);
65
81
  for (let i = 0; i < parts.length; i++) {
66
- let tIndex = parts[i].indexOf(Constant.Tilde);
67
- let dIndex = parts[i].indexOf(Constant.Dot);
68
- parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : (tIndex > 0 ? tIndex : parts[i].length));
82
+ const tIndex = parts[i].indexOf(Constant.Tilde);
83
+ const dIndex = parts[i].indexOf(Constant.Dot);
84
+ parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : tIndex > 0 ? tIndex : parts[i].length);
69
85
  }
70
86
  return parts.join(Constant.Separator);
71
87
  }
72
88
 
73
89
  // Check if the given input string has digits or excluded class names
74
90
  function filter(value: string): boolean {
75
- if (!value) { return false; } // Do not process empty strings
76
- if (excludeClassNames.some(x => value.toLowerCase().indexOf(x) >= 0)) { return false; }
91
+ if (!value) {
92
+ return false;
93
+ } // Do not process empty strings
94
+ if (excludeClassNames.some((x) => value.toLowerCase().indexOf(x) >= 0)) {
95
+ return false;
96
+ }
77
97
  for (let i = 0; i < value.length; i++) {
78
- let c = value.charCodeAt(i);
79
- if (c >= Character.Zero && c <= Character.Nine) { return false };
98
+ const c = value.charCodeAt(i);
99
+ if (c >= Character.Zero && c <= Character.Nine) {
100
+ return false;
101
+ }
80
102
  }
81
103
  return true;
82
104
  }
@@ -1,67 +1,70 @@
1
1
  import { Event } from "@clarity-types/data";
2
- import { StyleSheetOperation, StyleSheetState } from "@clarity-types/layout";
2
+ import { type IWindowWithOverrides, StyleSheetOperation, type StyleSheetState } from "@clarity-types/layout";
3
+ import * as core from "@src/core";
4
+ import config from "@src/core/config";
3
5
  import { time } from "@src/core/time";
4
6
  import { shortid } from "@src/data/metadata";
5
- import encode from "@src/layout/encode";
6
7
  import { getId } from "@src/layout/dom";
7
- import * as core from "@src/core";
8
- import config from "@src/core/config";
8
+ import encode from "@src/layout/encode";
9
9
  import { getCssRules } from "./node";
10
10
 
11
11
  export let sheetUpdateState: StyleSheetState[] = [];
12
12
  export let sheetAdoptionState: StyleSheetState[] = [];
13
- const styleSheetId = 'claritySheetId';
13
+ const styleSheetId = "claritySheetId";
14
14
  let styleSheetMap = {};
15
- let styleTimeMap: {[key: string]: number} = {};
15
+ let styleTimeMap: { [key: string]: number } = {};
16
16
  let documentNodes = [];
17
17
  let createdSheetIds = [];
18
18
 
19
- function proxyStyleRules(win: any) {
19
+ function proxyStyleRules(win: IWindowWithOverrides) {
20
20
  if ((config.lean && config.lite) || win === null || win === undefined) {
21
21
  return;
22
22
  }
23
-
23
+
24
24
  win.clarityOverrides = win.clarityOverrides || {};
25
25
 
26
- if (win['CSSStyleSheet'] && win.CSSStyleSheet.prototype) {
27
- if (win.clarityOverrides.replace === undefined) {
28
- win.clarityOverrides.replace = win.CSSStyleSheet.prototype.replace;
29
- win.CSSStyleSheet.prototype.replace = function(): Promise<CSSStyleSheet> {
26
+ if (win.CSSStyleSheet?.prototype) {
27
+ if (win.clarityOverrides.replace === undefined) {
28
+ win.clarityOverrides.replace = win.CSSStyleSheet.prototype.replace;
29
+ win.CSSStyleSheet.prototype.replace = function (...args): Promise<CSSStyleSheet> {
30
30
  if (core.active()) {
31
31
  // if we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it
32
32
  // and attached the sheet to a document. This way the timestamp of the style sheet creation will align
33
33
  // to when it is used in the document rather than potentially being misaligned during the traverse process.
34
34
  if (createdSheetIds.indexOf(this[styleSheetId]) > -1) {
35
- trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Replace, arguments[0]);
35
+ trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Replace, args[0]);
36
36
  }
37
37
  }
38
- return win.clarityOverrides.replace.apply(this, arguments);
38
+ return win.clarityOverrides.replace.apply(this, args);
39
39
  };
40
40
  }
41
41
 
42
- if (win.clarityOverrides.replaceSync === undefined) {
43
- win.clarityOverrides.replaceSync = win.CSSStyleSheet.prototype.replaceSync;
44
- win.CSSStyleSheet.prototype.replaceSync = function(): void {
42
+ if (win.clarityOverrides.replaceSync === undefined) {
43
+ win.clarityOverrides.replaceSync = win.CSSStyleSheet.prototype.replaceSync;
44
+ win.CSSStyleSheet.prototype.replaceSync = function (...args): void {
45
45
  if (core.active()) {
46
46
  // if we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it
47
47
  // and attached the sheet to a document. This way the timestamp of the style sheet creation will align
48
48
  // to when it is used in the document rather than potentially being misaligned during the traverse process.
49
49
  if (createdSheetIds.indexOf(this[styleSheetId]) > -1) {
50
- trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.ReplaceSync, arguments[0]);
51
- }
50
+ trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.ReplaceSync, args[0]);
51
+ }
52
52
  }
53
- return win.clarityOverrides.replaceSync.apply(this, arguments);
53
+ win.clarityOverrides.replaceSync.apply(this, args);
54
+ return;
54
55
  };
55
56
  }
56
- }
57
+ }
57
58
  }
58
59
 
59
60
  export function start(): void {
60
61
  proxyStyleRules(window);
61
62
  }
62
63
 
63
- export function checkDocumentStyles(documentNode: Document, timestamp: number): void {
64
- if (config.lean && config.lite) { return; }
64
+ export function checkDocumentStyles(documentNode: Document, cachedTimestamp: number): void {
65
+ if (config.lean && config.lite) {
66
+ return;
67
+ }
65
68
 
66
69
  if (documentNodes.indexOf(documentNode) === -1) {
67
70
  documentNodes.push(documentNode);
@@ -69,13 +72,13 @@ export function checkDocumentStyles(documentNode: Document, timestamp: number):
69
72
  proxyStyleRules(documentNode.defaultView);
70
73
  }
71
74
  }
72
- timestamp = timestamp || time();
75
+ const timestamp = cachedTimestamp || time();
73
76
  if (!documentNode?.adoptedStyleSheets) {
74
77
  // if we don't have adoptedStyledSheets on the Node passed to us, we can short circuit.
75
78
  return;
76
79
  }
77
- let currentStyleSheets: string[] = [];
78
- for (var styleSheet of documentNode.adoptedStyleSheets) {
80
+ const currentStyleSheets: string[] = [];
81
+ for (const styleSheet of documentNode.adoptedStyleSheets) {
79
82
  // If we haven't seen this style sheet on this page yet, we create a reference to it for the visualizer.
80
83
  // For SPA or times in which Clarity restarts on a given page, our visualizer would lose context
81
84
  // on the previously created style sheet for page N-1.
@@ -90,22 +93,27 @@ export function checkDocumentStyles(documentNode: Document, timestamp: number):
90
93
  currentStyleSheets.push(styleSheet[styleSheetId]);
91
94
  }
92
95
 
93
- let documentId = getId(documentNode, true);
96
+ const documentId = getId(documentNode, true);
94
97
  if (!styleSheetMap[documentId]) {
95
98
  styleSheetMap[documentId] = [];
96
99
  }
97
100
  if (!arraysEqual(currentStyleSheets, styleSheetMap[documentId])) {
98
101
  // Using -1 to signify the root document node as we don't track that as part of our nodeMap
99
- trackStyleAdoption(timestamp, documentNode == document ? -1 : getId(documentNode), StyleSheetOperation.SetAdoptedStyles, currentStyleSheets);
102
+ trackStyleAdoption(
103
+ timestamp,
104
+ documentNode === document ? -1 : getId(documentNode),
105
+ StyleSheetOperation.SetAdoptedStyles,
106
+ currentStyleSheets,
107
+ );
100
108
  styleSheetMap[documentId] = currentStyleSheets;
101
109
  styleTimeMap[documentId] = timestamp;
102
110
  }
103
111
  }
104
112
 
105
113
  export function compute(): void {
106
- for (var documentNode of documentNodes) {
107
- var docId = documentNode == document ? -1 : getId(documentNode);
108
- let ts = docId in styleTimeMap ? styleTimeMap[docId] : null;
114
+ for (const documentNode of documentNodes) {
115
+ const docId = documentNode === document ? -1 : getId(documentNode);
116
+ const ts = docId in styleTimeMap ? styleTimeMap[docId] : null;
109
117
  checkDocumentStyles(documentNode, ts);
110
118
  }
111
119
  }
@@ -130,8 +138,8 @@ function trackStyleChange(time: number, id: string, operation: StyleSheetOperati
130
138
  data: {
131
139
  id,
132
140
  operation,
133
- cssRules
134
- }
141
+ cssRules,
142
+ },
135
143
  });
136
144
 
137
145
  encode(Event.StyleSheetUpdate);
@@ -144,8 +152,8 @@ function trackStyleAdoption(time: number, id: number, operation: StyleSheetOpera
144
152
  data: {
145
153
  id,
146
154
  operation,
147
- newIds
148
- }
155
+ newIds,
156
+ },
149
157
  });
150
158
 
151
159
  encode(Event.StyleSheetAdoption);
@@ -157,4 +165,4 @@ function arraysEqual(a: string[], b: string[]): boolean {
157
165
  }
158
166
 
159
167
  return a.every((value, index) => value === b[index]);
160
- }
168
+ }
@@ -1,30 +1,34 @@
1
1
  import { Privacy } from "@clarity-types/core";
2
- import { Event } from "@clarity-types/data";
3
- import { TargetMetadata } from "@clarity-types/layout";
2
+ import type { Event } from "@clarity-types/data";
3
+ import type { TargetMetadata } from "@clarity-types/layout";
4
4
  import * as fraud from "@src/diagnostic/fraud";
5
- import * as region from "@src/layout/region";
6
5
  import * as dom from "@src/layout/dom";
7
6
  import * as mutation from "@src/layout/mutation";
7
+ import * as region from "@src/layout/region";
8
8
 
9
9
  export function target(evt: UIEvent): Node {
10
- let path = evt.composed && evt.composedPath ? evt.composedPath() : null;
11
- let node = (path && path.length > 0 ? path[0] : evt.target) as Node;
10
+ const path = evt.composed && evt.composedPath ? evt.composedPath() : null;
11
+ const node = (path && path.length > 0 ? path[0] : evt.target) as Node;
12
12
  mutation.active(); // Mark active periods of time so mutations can continue uninterrupted
13
13
  return node && node.nodeType === Node.DOCUMENT_NODE ? (node as Document).documentElement : node;
14
14
  }
15
15
 
16
16
  export function metadata(node: Node, event: Event, text: string = null): TargetMetadata {
17
17
  // If the node is null, we return a reserved value for id: 0. Valid assignment of id begins from 1+.
18
- let output: TargetMetadata = { id: 0, hash: null, privacy: Privacy.Text };
18
+ const output: TargetMetadata = { id: 0, hash: null, privacy: Privacy.Text };
19
19
  if (node) {
20
- let value = dom.get(node);
20
+ const value = dom.get(node);
21
21
  if (value !== null) {
22
- let metadata = value.metadata;
22
+ const metadata = value.metadata;
23
23
  output.id = value.id;
24
24
  output.hash = value.hash;
25
25
  output.privacy = metadata.privacy;
26
- if (value.region) { region.track(value.region, event); }
27
- if (metadata.fraud) { fraud.check(metadata.fraud, value.id, text || value.data.value); }
26
+ if (value.region) {
27
+ region.track(value.region, event);
28
+ }
29
+ if (metadata.fraud) {
30
+ fraud.check(metadata.fraud, value.id, text || value.data.value);
31
+ }
28
32
  }
29
33
  }
30
34
 
@@ -1,28 +1,34 @@
1
- import { Task, Timer } from "@clarity-types/core";
2
- import { Source } from "@clarity-types/layout";
1
+ import { Task, type Timer } from "@clarity-types/core";
2
+ import type { Source } from "@clarity-types/layout";
3
3
  import * as task from "@src/core/task";
4
4
  import node from "@src/layout/node";
5
5
 
6
- export default async function(root: Node, timer: Timer, source: Source, timestamp: number): Promise<void> {
7
- let queue = [root];
6
+ export default async function (root: Node, timer: Timer, source: Source, timestamp: number): Promise<void> {
7
+ const queue = [root];
8
8
  while (queue.length > 0) {
9
- let entry = queue.shift();
9
+ const entry = queue.shift();
10
10
  let next = entry.firstChild;
11
11
 
12
12
  while (next) {
13
13
  queue.push(next);
14
14
  next = next.nextSibling;
15
15
  }
16
-
16
+
17
17
  // Check the status of current task to see if we should yield before continuing
18
18
  let state = task.state(timer);
19
- if (state === Task.Wait) { state = await task.suspend(timer); }
20
- if (state === Task.Stop) { break; }
19
+ if (state === Task.Wait) {
20
+ state = await task.suspend(timer);
21
+ }
22
+ if (state === Task.Stop) {
23
+ break;
24
+ }
21
25
 
22
26
  // Check if processing a node gives us a pointer to one of its sub nodes for traversal
23
27
  // E.g. an element node may give us a pointer to traverse shadowDom if shadowRoot property is set
24
28
  // Or, an iframe from the same origin could give a pointer to it's document for traversing contents of iframe.
25
- let subnode = node(entry, source, timestamp);
26
- if (subnode) { queue.push(subnode); }
29
+ const subnode = node(entry, source, timestamp);
30
+ if (subnode) {
31
+ queue.push(subnode);
32
+ }
27
33
  }
28
- }
34
+ }
@@ -1,6 +1,6 @@
1
1
  export * from "@src/insight/blank";
2
2
 
3
- export let keys = [];
3
+ export const keys = [];
4
4
 
5
5
  /* Intentionally blank module with empty code */
6
6
  export function hashText(): void {}
@@ -1,11 +1,11 @@
1
- import {Event, Token} from "@clarity-types/data";
1
+ import { Event, type Token } from "@clarity-types/data";
2
2
  import { time } from "@src/core/time";
3
3
  import { queue } from "@src/data/upload";
4
4
  import * as navigation from "@src/performance/navigation";
5
5
 
6
- export default async function(type: Event): Promise<void> {
7
- let t = time();
8
- let tokens: Token[] = [t, type];
6
+ export default async function (type: Event): Promise<void> {
7
+ const t = time();
8
+ const tokens: Token[] = [t, type];
9
9
  switch (type) {
10
10
  case Event.Navigation:
11
11
  tokens.push(navigation.data.fetchStart);
@@ -1,8 +1,8 @@
1
- import { PerformanceEventTiming, Interaction } from '@clarity-types/data';
1
+ import type { Interaction, PerformanceEventTiming } from "@clarity-types/data";
2
2
 
3
3
  // Estimate variables to keep track of interactions
4
4
  let interactionCountEstimate = 0;
5
- let minKnownInteractionId = Infinity;
5
+ let minKnownInteractionId = Number.POSITIVE_INFINITY;
6
6
  let maxKnownInteractionId = 0;
7
7
 
8
8
  let prevInteractionCount = 0; // Used to track interaction count between pages
@@ -21,33 +21,25 @@ const longestInteractionMap: Map<number, Interaction> = new Map();
21
21
  * Dividing by 7 helps approximate the interaction count more accurately, since interaction IDs are spread out across a large range.
22
22
  */
23
23
  const countInteractions = (entry: PerformanceEventTiming) => {
24
- if ('interactionCount' in performance) {
25
- interactionCountEstimate = performance.interactionCount as number;
26
- return;
27
- }
28
-
29
- if (entry.interactionId) {
30
- minKnownInteractionId = Math.min(
31
- minKnownInteractionId,
32
- entry.interactionId
33
- );
34
- maxKnownInteractionId = Math.max(
35
- maxKnownInteractionId,
36
- entry.interactionId
37
- );
38
-
39
- interactionCountEstimate = maxKnownInteractionId
40
- ? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1
41
- : 0;
42
- }
24
+ if ("interactionCount" in performance) {
25
+ interactionCountEstimate = performance.interactionCount as number;
26
+ return;
27
+ }
28
+
29
+ if (entry.interactionId) {
30
+ minKnownInteractionId = Math.min(minKnownInteractionId, entry.interactionId);
31
+ maxKnownInteractionId = Math.max(maxKnownInteractionId, entry.interactionId);
32
+
33
+ interactionCountEstimate = maxKnownInteractionId ? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1 : 0;
34
+ }
43
35
  };
44
36
 
45
37
  const getInteractionCount = () => {
46
- return interactionCountEstimate || 0;
38
+ return interactionCountEstimate || 0;
47
39
  };
48
40
 
49
41
  const getInteractionCountForNavigation = () => {
50
- return getInteractionCount() - prevInteractionCount;
42
+ return getInteractionCount() - prevInteractionCount;
51
43
  };
52
44
 
53
45
  /**
@@ -55,71 +47,67 @@ const getInteractionCountForNavigation = () => {
55
47
  * the candidate interaction based on the current interaction count.
56
48
  * Dividing by 50 is a heuristic to estimate the 98th percentile (P98) interaction.
57
49
  * This assumes one out of every 50 interactions represents the P98 interaction.
58
- * By dividing the total interaction count by 50, we get an index to approximate
50
+ * By dividing the total interaction count by 50, we get an index to approximate
59
51
  * the slowest 2% of interactions, helping identify a likely P98 candidate.
60
52
  */
61
53
  export const estimateP98LongestInteraction = () => {
62
- if(!longestInteractionList.length){
63
- return -1;
64
- }
54
+ if (!longestInteractionList.length) {
55
+ return -1;
56
+ }
65
57
 
66
- const candidateInteractionIndex = Math.min(
67
- longestInteractionList.length - 1,
68
- Math.floor(getInteractionCountForNavigation() / 50)
69
- );
58
+ const candidateInteractionIndex = Math.min(longestInteractionList.length - 1, Math.floor(getInteractionCountForNavigation() / 50));
70
59
 
71
- return longestInteractionList[candidateInteractionIndex].latency;
60
+ return longestInteractionList[candidateInteractionIndex].latency;
72
61
  };
73
62
 
74
63
  /**
75
64
  * Resets the interaction tracking, usually called after navigation to a new page.
76
65
  */
77
66
  export const resetInteractions = () => {
78
- prevInteractionCount = getInteractionCount();
79
- longestInteractionList.length = 0;
80
- longestInteractionMap.clear();
67
+ prevInteractionCount = getInteractionCount();
68
+ longestInteractionList.length = 0;
69
+ longestInteractionMap.clear();
81
70
  };
82
71
 
83
72
  /**
84
73
  * Processes a PerformanceEventTiming entry by updating the longest interaction list.
85
74
  */
86
75
  export const processInteractionEntry = (entry: PerformanceEventTiming) => {
87
- // Ignore entries with 0 interactionId or very short durations
88
- if (!entry.interactionId || entry.duration < DEFAULT_DURATION_THRESHOLD) {
89
- return;
90
- }
91
-
92
- countInteractions(entry);
93
-
94
- const minLongestInteraction =
95
- longestInteractionList[longestInteractionList.length - 1];
96
-
97
- const existingInteraction = longestInteractionMap.get(entry.interactionId!);
98
-
99
- // Either update existing, add new, or replace shortest interaction if necessary
100
- if (
101
- existingInteraction ||
102
- longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
103
- entry.duration > minLongestInteraction?.latency
104
- ) {
105
- if (!existingInteraction) {
106
- const interaction = {
107
- id: entry.interactionId,
108
- latency: entry.duration,
109
- };
110
- longestInteractionMap.set(interaction.id, interaction);
111
- longestInteractionList.push(interaction);
112
- } else if (entry.duration > existingInteraction.latency) {
113
- existingInteraction.latency = entry.duration;
76
+ // Ignore entries with 0 interactionId or very short durations
77
+ if (!entry.interactionId || entry.duration < DEFAULT_DURATION_THRESHOLD) {
78
+ return;
114
79
  }
115
80
 
116
- longestInteractionList.sort((a, b) => b.latency - a.latency);
117
-
118
- // Trim the list to the maximum number of interactions to consider
119
- if (longestInteractionList.length > MAX_INTERACTIONS_TO_CONSIDER) {
120
- longestInteractionList
121
- .splice(MAX_INTERACTIONS_TO_CONSIDER)
122
- .forEach((i) => longestInteractionMap.delete(i.id));
81
+ countInteractions(entry);
82
+
83
+ const minLongestInteraction = longestInteractionList[longestInteractionList.length - 1];
84
+
85
+ const existingInteraction = longestInteractionMap.get(entry.interactionId);
86
+
87
+ // Either update existing, add new, or replace shortest interaction if necessary
88
+ if (
89
+ existingInteraction ||
90
+ longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
91
+ entry.duration > minLongestInteraction?.latency
92
+ ) {
93
+ if (!existingInteraction) {
94
+ const interaction = {
95
+ id: entry.interactionId,
96
+ latency: entry.duration,
97
+ };
98
+ longestInteractionMap.set(interaction.id, interaction);
99
+ longestInteractionList.push(interaction);
100
+ } else if (entry.duration > existingInteraction.latency) {
101
+ existingInteraction.latency = entry.duration;
102
+ }
103
+
104
+ longestInteractionList.sort((a, b) => b.latency - a.latency);
105
+
106
+ // Trim the list to the maximum number of interactions to consider
107
+ if (longestInteractionList.length > MAX_INTERACTIONS_TO_CONSIDER) {
108
+ for (const i of longestInteractionList.splice(MAX_INTERACTIONS_TO_CONSIDER)) {
109
+ longestInteractionMap.delete(i.id);
110
+ }
111
+ }
123
112
  }
124
- }
125
113
  };
@@ -1,5 +1,5 @@
1
1
  import { Event } from "@clarity-types/data";
2
- import { NavigationData } from "@clarity-types/performance";
2
+ import type { NavigationData } from "@clarity-types/performance";
3
3
  import encode from "./encode";
4
4
 
5
5
  export let data: NavigationData = null;
@@ -25,7 +25,7 @@ export function compute(entry: PerformanceNavigationTiming): void {
25
25
  type: entry.type,
26
26
  protocol: entry.nextHopProtocol,
27
27
  encodedSize: entry.encodedBodySize ? entry.encodedBodySize : 0,
28
- decodedSize: entry.decodedBodySize ? entry.decodedBodySize : 0
28
+ decodedSize: entry.decodedBodySize ? entry.decodedBodySize : 0,
29
29
  };
30
30
  encode(Event.Navigation);
31
31
  }