@gemx-dev/clarity-js 0.8.39

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 (118) hide show
  1. package/README.md +1 -0
  2. package/build/clarity.extended.js +1 -0
  3. package/build/clarity.insight.js +1 -0
  4. package/build/clarity.js +6433 -0
  5. package/build/clarity.livechat.js +1 -0
  6. package/build/clarity.min.js +1 -0
  7. package/build/clarity.module.js +6429 -0
  8. package/build/clarity.performance.js +1 -0
  9. package/build/dynamic/clarity.crisp.js +1 -0
  10. package/build/dynamic/clarity.tidio.js +1 -0
  11. package/package.json +55 -0
  12. package/rollup.config.ts +161 -0
  13. package/src/clarity.ts +71 -0
  14. package/src/core/api.ts +8 -0
  15. package/src/core/config.ts +29 -0
  16. package/src/core/copy.ts +3 -0
  17. package/src/core/dynamic.ts +57 -0
  18. package/src/core/event.ts +53 -0
  19. package/src/core/hash.ts +19 -0
  20. package/src/core/history.ts +71 -0
  21. package/src/core/index.ts +81 -0
  22. package/src/core/measure.ts +19 -0
  23. package/src/core/report.ts +35 -0
  24. package/src/core/scrub.ts +202 -0
  25. package/src/core/task.ts +181 -0
  26. package/src/core/throttle.ts +46 -0
  27. package/src/core/time.ts +26 -0
  28. package/src/core/timeout.ts +10 -0
  29. package/src/core/version.ts +2 -0
  30. package/src/data/baseline.ts +162 -0
  31. package/src/data/compress.ts +31 -0
  32. package/src/data/consent.ts +75 -0
  33. package/src/data/custom.ts +23 -0
  34. package/src/data/dimension.ts +53 -0
  35. package/src/data/encode.ts +155 -0
  36. package/src/data/envelope.ts +53 -0
  37. package/src/data/extract.ts +211 -0
  38. package/src/data/index.ts +50 -0
  39. package/src/data/limit.ts +44 -0
  40. package/src/data/metadata.ts +408 -0
  41. package/src/data/metric.ts +51 -0
  42. package/src/data/ping.ts +36 -0
  43. package/src/data/signal.ts +30 -0
  44. package/src/data/summary.ts +34 -0
  45. package/src/data/token.ts +39 -0
  46. package/src/data/upgrade.ts +44 -0
  47. package/src/data/upload.ts +333 -0
  48. package/src/data/variable.ts +84 -0
  49. package/src/diagnostic/encode.ts +40 -0
  50. package/src/diagnostic/fraud.ts +37 -0
  51. package/src/diagnostic/index.ts +13 -0
  52. package/src/diagnostic/internal.ts +28 -0
  53. package/src/diagnostic/script.ts +35 -0
  54. package/src/dynamic/agent/blank.ts +2 -0
  55. package/src/dynamic/agent/crisp.ts +40 -0
  56. package/src/dynamic/agent/encode.ts +25 -0
  57. package/src/dynamic/agent/index.ts +8 -0
  58. package/src/dynamic/agent/livechat.ts +58 -0
  59. package/src/dynamic/agent/tidio.ts +44 -0
  60. package/src/global.ts +6 -0
  61. package/src/index.ts +9 -0
  62. package/src/insight/blank.ts +14 -0
  63. package/src/insight/encode.ts +61 -0
  64. package/src/insight/snapshot.ts +115 -0
  65. package/src/interaction/change.ts +38 -0
  66. package/src/interaction/click.ts +163 -0
  67. package/src/interaction/clipboard.ts +32 -0
  68. package/src/interaction/encode.ts +207 -0
  69. package/src/interaction/focus.ts +25 -0
  70. package/src/interaction/index.ts +60 -0
  71. package/src/interaction/input.ts +58 -0
  72. package/src/interaction/pointer.ts +137 -0
  73. package/src/interaction/resize.ts +50 -0
  74. package/src/interaction/scroll.ts +129 -0
  75. package/src/interaction/selection.ts +66 -0
  76. package/src/interaction/submit.ts +30 -0
  77. package/src/interaction/timeline.ts +69 -0
  78. package/src/interaction/unload.ts +26 -0
  79. package/src/interaction/visibility.ts +28 -0
  80. package/src/layout/animation.ts +133 -0
  81. package/src/layout/custom.ts +43 -0
  82. package/src/layout/discover.ts +31 -0
  83. package/src/layout/document.ts +46 -0
  84. package/src/layout/dom.ts +439 -0
  85. package/src/layout/encode.ts +154 -0
  86. package/src/layout/index.ts +42 -0
  87. package/src/layout/mutation.ts +412 -0
  88. package/src/layout/node.ts +294 -0
  89. package/src/layout/offset.ts +19 -0
  90. package/src/layout/region.ts +151 -0
  91. package/src/layout/schema.ts +63 -0
  92. package/src/layout/selector.ts +82 -0
  93. package/src/layout/style.ts +160 -0
  94. package/src/layout/target.ts +32 -0
  95. package/src/layout/traverse.ts +28 -0
  96. package/src/performance/blank.ts +10 -0
  97. package/src/performance/encode.ts +31 -0
  98. package/src/performance/index.ts +12 -0
  99. package/src/performance/interaction.ts +125 -0
  100. package/src/performance/navigation.ts +31 -0
  101. package/src/performance/observer.ts +112 -0
  102. package/src/queue.ts +33 -0
  103. package/test/core.test.ts +139 -0
  104. package/test/helper.ts +167 -0
  105. package/test/html/core.html +34 -0
  106. package/test/stub.test.ts +7 -0
  107. package/test/tsconfig.test.json +6 -0
  108. package/tsconfig.json +21 -0
  109. package/tslint.json +33 -0
  110. package/types/agent.d.ts +39 -0
  111. package/types/core.d.ts +150 -0
  112. package/types/data.d.ts +568 -0
  113. package/types/diagnostic.d.ts +24 -0
  114. package/types/global.d.ts +30 -0
  115. package/types/index.d.ts +40 -0
  116. package/types/interaction.d.ts +174 -0
  117. package/types/layout.d.ts +277 -0
  118. package/types/performance.d.ts +31 -0
@@ -0,0 +1,160 @@
1
+ import { Event } from "@clarity-types/data";
2
+ import { StyleSheetOperation, StyleSheetState } from "@clarity-types/layout";
3
+ import { time } from "@src/core/time";
4
+ import { shortid } from "@src/data/metadata";
5
+ import encode from "@src/layout/encode";
6
+ import { getId } from "@src/layout/dom";
7
+ import * as core from "@src/core";
8
+ import config from "@src/core/config";
9
+ import { getCssRules } from "./node";
10
+
11
+ export let sheetUpdateState: StyleSheetState[] = [];
12
+ export let sheetAdoptionState: StyleSheetState[] = [];
13
+ const styleSheetId = 'claritySheetId';
14
+ let styleSheetMap = {};
15
+ let styleTimeMap: {[key: string]: number} = {};
16
+ let documentNodes = [];
17
+ let createdSheetIds = [];
18
+
19
+ function proxyStyleRules(win: any) {
20
+ if ((config.lean && config.lite) || win === null || win === undefined) {
21
+ return;
22
+ }
23
+
24
+ win.clarityOverrides = win.clarityOverrides || {};
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> {
30
+ if (core.active()) {
31
+ // if we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it
32
+ // and attached the sheet to a document. This way the timestamp of the style sheet creation will align
33
+ // to when it is used in the document rather than potentially being misaligned during the traverse process.
34
+ if (createdSheetIds.indexOf(this[styleSheetId]) > -1) {
35
+ trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Replace, arguments[0]);
36
+ }
37
+ }
38
+ return win.clarityOverrides.replace.apply(this, arguments);
39
+ };
40
+ }
41
+
42
+ if (win.clarityOverrides.replaceSync === undefined) {
43
+ win.clarityOverrides.replaceSync = win.CSSStyleSheet.prototype.replaceSync;
44
+ win.CSSStyleSheet.prototype.replaceSync = function(): void {
45
+ if (core.active()) {
46
+ // if we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it
47
+ // and attached the sheet to a document. This way the timestamp of the style sheet creation will align
48
+ // to when it is used in the document rather than potentially being misaligned during the traverse process.
49
+ if (createdSheetIds.indexOf(this[styleSheetId]) > -1) {
50
+ trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.ReplaceSync, arguments[0]);
51
+ }
52
+ }
53
+ return win.clarityOverrides.replaceSync.apply(this, arguments);
54
+ };
55
+ }
56
+ }
57
+ }
58
+
59
+ export function start(): void {
60
+ proxyStyleRules(window);
61
+ }
62
+
63
+ export function checkDocumentStyles(documentNode: Document, timestamp: number): void {
64
+ if (config.lean && config.lite) { return; }
65
+
66
+ if (documentNodes.indexOf(documentNode) === -1) {
67
+ documentNodes.push(documentNode);
68
+ if (documentNode.defaultView) {
69
+ proxyStyleRules(documentNode.defaultView);
70
+ }
71
+ }
72
+ timestamp = timestamp || time();
73
+ if (!documentNode?.adoptedStyleSheets) {
74
+ // if we don't have adoptedStyledSheets on the Node passed to us, we can short circuit.
75
+ return;
76
+ }
77
+ let currentStyleSheets: string[] = [];
78
+ for (var styleSheet of documentNode.adoptedStyleSheets) {
79
+ // If we haven't seen this style sheet on this page yet, we create a reference to it for the visualizer.
80
+ // For SPA or times in which Clarity restarts on a given page, our visualizer would lose context
81
+ // on the previously created style sheet for page N-1.
82
+ // Then we synthetically call replaceSync with its contents to bootstrap it
83
+ if (!styleSheet[styleSheetId] || createdSheetIds.indexOf(styleSheet[styleSheetId]) === -1) {
84
+ styleSheet[styleSheetId] = shortid();
85
+ createdSheetIds.push(styleSheet[styleSheetId]);
86
+ trackStyleChange(timestamp, styleSheet[styleSheetId], StyleSheetOperation.Create);
87
+ trackStyleChange(timestamp, styleSheet[styleSheetId], StyleSheetOperation.ReplaceSync, getCssRules(styleSheet));
88
+ }
89
+
90
+ currentStyleSheets.push(styleSheet[styleSheetId]);
91
+ }
92
+
93
+ let documentId = getId(documentNode, true);
94
+ if (!styleSheetMap[documentId]) {
95
+ styleSheetMap[documentId] = [];
96
+ }
97
+ if (!arraysEqual(currentStyleSheets, styleSheetMap[documentId])) {
98
+ // 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);
100
+ styleSheetMap[documentId] = currentStyleSheets;
101
+ styleTimeMap[documentId] = timestamp;
102
+ }
103
+ }
104
+
105
+ 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;
109
+ checkDocumentStyles(documentNode, ts);
110
+ }
111
+ }
112
+
113
+ export function reset(): void {
114
+ sheetAdoptionState = [];
115
+ sheetUpdateState = [];
116
+ }
117
+
118
+ export function stop(): void {
119
+ styleSheetMap = {};
120
+ styleTimeMap = {};
121
+ documentNodes = [];
122
+ createdSheetIds = [];
123
+ reset();
124
+ }
125
+
126
+ function trackStyleChange(time: number, id: string, operation: StyleSheetOperation, cssRules?: string): void {
127
+ sheetUpdateState.push({
128
+ time,
129
+ event: Event.StyleSheetUpdate,
130
+ data: {
131
+ id,
132
+ operation,
133
+ cssRules
134
+ }
135
+ });
136
+
137
+ encode(Event.StyleSheetUpdate);
138
+ }
139
+
140
+ function trackStyleAdoption(time: number, id: number, operation: StyleSheetOperation, newIds: string[]): void {
141
+ sheetAdoptionState.push({
142
+ time,
143
+ event: Event.StyleSheetAdoption,
144
+ data: {
145
+ id,
146
+ operation,
147
+ newIds
148
+ }
149
+ });
150
+
151
+ encode(Event.StyleSheetAdoption);
152
+ }
153
+
154
+ function arraysEqual(a: string[], b: string[]): boolean {
155
+ if (a.length !== b.length) {
156
+ return false;
157
+ }
158
+
159
+ return a.every((value, index) => value === b[index]);
160
+ }
@@ -0,0 +1,32 @@
1
+ import { Privacy } from "@clarity-types/core";
2
+ import { Event } from "@clarity-types/data";
3
+ import { TargetMetadata } from "@clarity-types/layout";
4
+ import * as fraud from "@src/diagnostic/fraud";
5
+ import * as region from "@src/layout/region";
6
+ import * as dom from "@src/layout/dom";
7
+ import * as mutation from "@src/layout/mutation";
8
+
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;
12
+ mutation.active(); // Mark active periods of time so mutations can continue uninterrupted
13
+ return node && node.nodeType === Node.DOCUMENT_NODE ? (node as Document).documentElement : node;
14
+ }
15
+
16
+ export function metadata(node: Node, event: Event, text: string = null): TargetMetadata {
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 };
19
+ if (node) {
20
+ let value = dom.get(node);
21
+ if (value !== null) {
22
+ let metadata = value.metadata;
23
+ output.id = value.id;
24
+ output.hash = value.hash;
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); }
28
+ }
29
+ }
30
+
31
+ return output;
32
+ }
@@ -0,0 +1,28 @@
1
+ import { Task, Timer } from "@clarity-types/core";
2
+ import { Source } from "@clarity-types/layout";
3
+ import * as task from "@src/core/task";
4
+ import node from "@src/layout/node";
5
+
6
+ export default async function(root: Node, timer: Timer, source: Source, timestamp: number): Promise<void> {
7
+ let queue = [root];
8
+ while (queue.length > 0) {
9
+ let entry = queue.shift();
10
+ let next = entry.firstChild;
11
+
12
+ while (next) {
13
+ queue.push(next);
14
+ next = next.nextSibling;
15
+ }
16
+
17
+ // Check the status of current task to see if we should yield before continuing
18
+ let state = task.state(timer);
19
+ if (state === Task.Wait) { state = await task.suspend(timer); }
20
+ if (state === Task.Stop) { break; }
21
+
22
+ // Check if processing a node gives us a pointer to one of its sub nodes for traversal
23
+ // E.g. an element node may give us a pointer to traverse shadowDom if shadowRoot property is set
24
+ // 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); }
27
+ }
28
+ }
@@ -0,0 +1,10 @@
1
+ export * from "@src/insight/blank";
2
+
3
+ export let keys = [];
4
+
5
+ /* Intentionally blank module with empty code */
6
+ export function hashText(): void {}
7
+ export function trigger(): void {}
8
+ export function track(): void {}
9
+ export function event(): void {}
10
+ export function register(): void {}
@@ -0,0 +1,31 @@
1
+ import {Event, Token} from "@clarity-types/data";
2
+ import { time } from "@src/core/time";
3
+ import { queue } from "@src/data/upload";
4
+ import * as navigation from "@src/performance/navigation";
5
+
6
+ export default async function(type: Event): Promise<void> {
7
+ let t = time();
8
+ let tokens: Token[] = [t, type];
9
+ switch (type) {
10
+ case Event.Navigation:
11
+ tokens.push(navigation.data.fetchStart);
12
+ tokens.push(navigation.data.connectStart);
13
+ tokens.push(navigation.data.connectEnd);
14
+ tokens.push(navigation.data.requestStart);
15
+ tokens.push(navigation.data.responseStart);
16
+ tokens.push(navigation.data.responseEnd);
17
+ tokens.push(navigation.data.domInteractive);
18
+ tokens.push(navigation.data.domComplete);
19
+ tokens.push(navigation.data.loadEventStart);
20
+ tokens.push(navigation.data.loadEventEnd);
21
+ tokens.push(navigation.data.redirectCount);
22
+ tokens.push(navigation.data.size);
23
+ tokens.push(navigation.data.type);
24
+ tokens.push(navigation.data.protocol);
25
+ tokens.push(navigation.data.encodedSize);
26
+ tokens.push(navigation.data.decodedSize);
27
+ navigation.reset();
28
+ queue(tokens);
29
+ break;
30
+ }
31
+ }
@@ -0,0 +1,12 @@
1
+ import * as navigation from "@src/performance/navigation";
2
+ import * as observer from "@src/performance/observer";
3
+
4
+ export function start(): void {
5
+ navigation.reset();
6
+ observer.start();
7
+ }
8
+
9
+ export function stop(): void {
10
+ observer.stop();
11
+ navigation.reset();
12
+ }
@@ -0,0 +1,125 @@
1
+ import { PerformanceEventTiming, Interaction } from '@clarity-types/data';
2
+
3
+ // Estimate variables to keep track of interactions
4
+ let interactionCountEstimate = 0;
5
+ let minKnownInteractionId = Infinity;
6
+ let maxKnownInteractionId = 0;
7
+
8
+ let prevInteractionCount = 0; // Used to track interaction count between pages
9
+
10
+ const MAX_INTERACTIONS_TO_CONSIDER = 10; // Maximum number of interactions we consider for INP
11
+ const DEFAULT_DURATION_THRESHOLD = 40; // Threshold to ignore very short interactions
12
+
13
+ // List to store the longest interaction events
14
+ const longestInteractionList: Interaction[] = [];
15
+ // Map to track interactions by their ID, ensuring we handle duplicates
16
+ const longestInteractionMap: Map<number, Interaction> = new Map();
17
+
18
+ /**
19
+ * Update the approx number of interactions estimate count if the interactionCount is not supported.
20
+ * The difference between `maxKnownInteractionId` and `minKnownInteractionId` gives us a rough range of how many interactions have occurred.
21
+ * Dividing by 7 helps approximate the interaction count more accurately, since interaction IDs are spread out across a large range.
22
+ */
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
+ }
43
+ };
44
+
45
+ const getInteractionCount = () => {
46
+ return interactionCountEstimate || 0;
47
+ };
48
+
49
+ const getInteractionCountForNavigation = () => {
50
+ return getInteractionCount() - prevInteractionCount;
51
+ };
52
+
53
+ /**
54
+ * Estimates the 98th percentile (P98) of the longest interactions by selecting
55
+ * the candidate interaction based on the current interaction count.
56
+ * Dividing by 50 is a heuristic to estimate the 98th percentile (P98) interaction.
57
+ * 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
59
+ * the slowest 2% of interactions, helping identify a likely P98 candidate.
60
+ */
61
+ export const estimateP98LongestInteraction = () => {
62
+ if(!longestInteractionList.length){
63
+ return -1;
64
+ }
65
+
66
+ const candidateInteractionIndex = Math.min(
67
+ longestInteractionList.length - 1,
68
+ Math.floor(getInteractionCountForNavigation() / 50)
69
+ );
70
+
71
+ return longestInteractionList[candidateInteractionIndex].latency;
72
+ };
73
+
74
+ /**
75
+ * Resets the interaction tracking, usually called after navigation to a new page.
76
+ */
77
+ export const resetInteractions = () => {
78
+ prevInteractionCount = getInteractionCount();
79
+ longestInteractionList.length = 0;
80
+ longestInteractionMap.clear();
81
+ };
82
+
83
+ /**
84
+ * Processes a PerformanceEventTiming entry by updating the longest interaction list.
85
+ */
86
+ 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;
114
+ }
115
+
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));
123
+ }
124
+ }
125
+ };
@@ -0,0 +1,31 @@
1
+ import { Event } from "@clarity-types/data";
2
+ import { NavigationData } from "@clarity-types/performance";
3
+ import encode from "./encode";
4
+
5
+ export let data: NavigationData = null;
6
+
7
+ export function reset(): void {
8
+ data = null;
9
+ }
10
+
11
+ export function compute(entry: PerformanceNavigationTiming): void {
12
+ data = {
13
+ fetchStart: Math.round(entry.fetchStart),
14
+ connectStart: Math.round(entry.connectStart),
15
+ connectEnd: Math.round(entry.connectEnd),
16
+ requestStart: Math.round(entry.requestStart),
17
+ responseStart: Math.round(entry.responseStart),
18
+ responseEnd: Math.round(entry.responseEnd),
19
+ domInteractive: Math.round(entry.domInteractive),
20
+ domComplete: Math.round(entry.domComplete),
21
+ loadEventStart: Math.round(entry.loadEventStart),
22
+ loadEventEnd: Math.round(entry.loadEventEnd),
23
+ redirectCount: Math.round(entry.redirectCount),
24
+ size: entry.transferSize ? entry.transferSize : 0,
25
+ type: entry.type,
26
+ protocol: entry.nextHopProtocol,
27
+ encodedSize: entry.encodedBodySize ? entry.encodedBodySize : 0,
28
+ decodedSize: entry.decodedBodySize ? entry.decodedBodySize : 0
29
+ };
30
+ encode(Event.Navigation);
31
+ }
@@ -0,0 +1,112 @@
1
+ import { Code, Constant, Dimension, Metric, Severity, PerformanceEventTiming } from "@clarity-types/data";
2
+ import config from "@src/core/config";
3
+ import { bind } from "@src/core/event";
4
+ import measure from "@src/core/measure";
5
+ import { setTimeout } from "@src/core/timeout";
6
+ import * as dimension from "@src/data/dimension";
7
+ import * as metric from "@src/data/metric";
8
+ import * as internal from "@src/diagnostic/internal";
9
+ import * as navigation from "@src/performance/navigation";
10
+ import * as interaction from "@src/performance/interaction";
11
+
12
+ let observer: PerformanceObserver;
13
+ const types: string[] = [Constant.Navigation, Constant.Resource, Constant.LongTask, Constant.FID, Constant.CLS, Constant.LCP, Constant.PerformanceEventTiming];
14
+
15
+ export function start(): void {
16
+ // Capture connection properties, if available
17
+ if (navigator && navigator["connection"]) {
18
+ dimension.log(Dimension.ConnectionType, navigator["connection"]["effectiveType"]);
19
+ }
20
+
21
+ // Check the browser support performance observer as a pre-requisite for any performance measurement
22
+ if (window["PerformanceObserver"] && PerformanceObserver.supportedEntryTypes) {
23
+ // Start monitoring performance data after page has finished loading.
24
+ // If the document.readyState is not yet complete, we intentionally call observe using a setTimeout.
25
+ // This allows us to capture loadEventEnd on navigation timeline.
26
+ if (document.readyState !== "complete") {
27
+ bind(window, "load", setTimeout.bind(this, observe, 0));
28
+ } else { observe(); }
29
+ } else { internal.log(Code.PerformanceObserver, Severity.Info); }
30
+ }
31
+
32
+ function observe(): void {
33
+ // Some browsers will throw an error for unsupported entryType, e.g. "layout-shift"
34
+ // In those cases, we log it as a warning and continue with rest of the Clarity processing
35
+ try {
36
+ if (observer) { observer.disconnect(); }
37
+ observer = new PerformanceObserver(measure(handle) as PerformanceObserverCallback);
38
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver/observe
39
+ // "buffered" flag indicates whether buffered entries should be queued into the observer's buffer.
40
+ // It must only be used only with the "type" option, and cannot be used with entryTypes.
41
+ // This is why we need to individually "observe" each supported type
42
+ for (let x of types) {
43
+ if (PerformanceObserver.supportedEntryTypes.indexOf(x) >= 0) {
44
+ // Initialize CLS with a value of zero. It's possible (and recommended) for sites to not have any cumulative layout shift.
45
+ // In those cases, we want to still initialize the metric in Clarity
46
+ if (x === Constant.CLS) { metric.sum(Metric.CumulativeLayoutShift, 0); }
47
+ observer.observe({type: x, buffered: true});
48
+ }
49
+ }
50
+ } catch { internal.log(Code.PerformanceObserver, Severity.Warning); }
51
+ }
52
+
53
+ function handle(entries: PerformanceObserverEntryList): void {
54
+ process(entries.getEntries());
55
+ }
56
+
57
+ function process(entries: PerformanceEntryList): void {
58
+ let visible = "visibilityState" in document ? document.visibilityState === "visible" : true;
59
+ for (let i = 0; i < entries.length; i++) {
60
+ let entry = entries[i];
61
+ switch (entry.entryType) {
62
+ case Constant.Navigation:
63
+ navigation.compute(entry as PerformanceNavigationTiming);
64
+ break;
65
+ case Constant.Resource:
66
+ let name = entry.name;
67
+ dimension.log(Dimension.NetworkHosts, host(name));
68
+ if (name === config.upload || name === config.fallback) { metric.max(Metric.UploadTime, entry.duration); }
69
+ break;
70
+ case Constant.LongTask:
71
+ metric.count(Metric.LongTaskCount);
72
+ break;
73
+ case Constant.FID:
74
+ if (visible) { metric.max(Metric.FirstInputDelay, entry["processingStart"] - entry.startTime); }
75
+ break;
76
+ case Constant.PerformanceEventTiming:
77
+ if (visible && 'PerformanceEventTiming' in window && 'interactionId' in PerformanceEventTiming.prototype)
78
+ {
79
+ interaction.processInteractionEntry(entry as PerformanceEventTiming);
80
+ // Logging it as dimension because we're always looking for the last value.
81
+ dimension.log(Dimension.InteractionNextPaint, interaction.estimateP98LongestInteraction().toString());
82
+ }
83
+ break;
84
+ case Constant.CLS:
85
+ // Scale the value to avoid sending back floating point number
86
+ if (visible && !entry["hadRecentInput"]) { metric.sum(Metric.CumulativeLayoutShift, entry["value"] * 1000); }
87
+ break;
88
+ case Constant.LCP:
89
+ if (visible) { metric.max(Metric.LargestPaint, entry.startTime); }
90
+ break;
91
+ }
92
+ }
93
+ }
94
+
95
+ export function stop(): void {
96
+ if (observer) { observer.disconnect(); }
97
+ observer = null;
98
+ interaction.resetInteractions();
99
+ anchorCache = null;
100
+ }
101
+
102
+ // Cached anchor element for optimal performance & memory management
103
+ let anchorCache: HTMLAnchorElement | null = null;
104
+
105
+ function host(url: string): string {
106
+ if (!anchorCache) {
107
+ anchorCache = document.createElement("a");
108
+ }
109
+
110
+ anchorCache.href = url;
111
+ return anchorCache.host;
112
+ }
package/src/queue.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { Constant } from "@clarity-types/data";
2
+ import * as clarity from "@src/clarity";
3
+
4
+ const w = window;
5
+ const c = Constant.Clarity;
6
+
7
+ export function setup() {
8
+ // Start queuing up calls while Clarity is inactive and we are in a browser enviornment
9
+ if (typeof w !== "undefined") {
10
+ w[c] = function() {
11
+ (w[c].q = w[c].q || []).push(arguments);
12
+ // if the start function was called, don't queue it and instead process the queue
13
+ arguments[0] === "start" && w[c].q.unshift(w[c].q.pop()) && process();
14
+ };
15
+ }
16
+ }
17
+
18
+ export function process() {
19
+ if (typeof w !== "undefined") {
20
+ // Do not execute or reset global "clarity" variable if a version of Clarity is already running on the page
21
+ if (w[c] && w[c].v) { return console.warn("Error CL001: Multiple Clarity tags detected."); }
22
+
23
+ // Expose clarity in a browser environment
24
+ // To be efficient about queuing up operations while Clarity is wiring up, we expose clarity.*(args) => clarity(*, args);
25
+ // This allows us to reprocess any calls that we missed once Clarity is available on the page
26
+ // Once Clarity script bundle is loaded on the page, we also initialize a "v" property that holds current version
27
+ // We use the presence or absence of "v" to determine if we are attempting to run a duplicate instance
28
+ let queue = w[c] ? (w[c].q || []) : [];
29
+ w[c] = function(method: string, ...args: any[]): void { return clarity[method](...args); }
30
+ w[c].v = clarity.version;
31
+ while (queue.length > 0) { w[c](...queue.shift()); }
32
+ }
33
+ }