@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.
- package/README.md +1 -0
- package/build/clarity.extended.js +1 -0
- package/build/clarity.insight.js +1 -0
- package/build/clarity.js +6433 -0
- package/build/clarity.livechat.js +1 -0
- package/build/clarity.min.js +1 -0
- package/build/clarity.module.js +6429 -0
- package/build/clarity.performance.js +1 -0
- package/build/dynamic/clarity.crisp.js +1 -0
- package/build/dynamic/clarity.tidio.js +1 -0
- package/package.json +55 -0
- package/rollup.config.ts +161 -0
- package/src/clarity.ts +71 -0
- package/src/core/api.ts +8 -0
- package/src/core/config.ts +29 -0
- package/src/core/copy.ts +3 -0
- package/src/core/dynamic.ts +57 -0
- package/src/core/event.ts +53 -0
- package/src/core/hash.ts +19 -0
- package/src/core/history.ts +71 -0
- package/src/core/index.ts +81 -0
- package/src/core/measure.ts +19 -0
- package/src/core/report.ts +35 -0
- package/src/core/scrub.ts +202 -0
- package/src/core/task.ts +181 -0
- package/src/core/throttle.ts +46 -0
- package/src/core/time.ts +26 -0
- package/src/core/timeout.ts +10 -0
- package/src/core/version.ts +2 -0
- package/src/data/baseline.ts +162 -0
- package/src/data/compress.ts +31 -0
- package/src/data/consent.ts +75 -0
- package/src/data/custom.ts +23 -0
- package/src/data/dimension.ts +53 -0
- package/src/data/encode.ts +155 -0
- package/src/data/envelope.ts +53 -0
- package/src/data/extract.ts +211 -0
- package/src/data/index.ts +50 -0
- package/src/data/limit.ts +44 -0
- package/src/data/metadata.ts +408 -0
- package/src/data/metric.ts +51 -0
- package/src/data/ping.ts +36 -0
- package/src/data/signal.ts +30 -0
- package/src/data/summary.ts +34 -0
- package/src/data/token.ts +39 -0
- package/src/data/upgrade.ts +44 -0
- package/src/data/upload.ts +333 -0
- package/src/data/variable.ts +84 -0
- package/src/diagnostic/encode.ts +40 -0
- package/src/diagnostic/fraud.ts +37 -0
- package/src/diagnostic/index.ts +13 -0
- package/src/diagnostic/internal.ts +28 -0
- package/src/diagnostic/script.ts +35 -0
- package/src/dynamic/agent/blank.ts +2 -0
- package/src/dynamic/agent/crisp.ts +40 -0
- package/src/dynamic/agent/encode.ts +25 -0
- package/src/dynamic/agent/index.ts +8 -0
- package/src/dynamic/agent/livechat.ts +58 -0
- package/src/dynamic/agent/tidio.ts +44 -0
- package/src/global.ts +6 -0
- package/src/index.ts +9 -0
- package/src/insight/blank.ts +14 -0
- package/src/insight/encode.ts +61 -0
- package/src/insight/snapshot.ts +115 -0
- package/src/interaction/change.ts +38 -0
- package/src/interaction/click.ts +163 -0
- package/src/interaction/clipboard.ts +32 -0
- package/src/interaction/encode.ts +207 -0
- package/src/interaction/focus.ts +25 -0
- package/src/interaction/index.ts +60 -0
- package/src/interaction/input.ts +58 -0
- package/src/interaction/pointer.ts +137 -0
- package/src/interaction/resize.ts +50 -0
- package/src/interaction/scroll.ts +129 -0
- package/src/interaction/selection.ts +66 -0
- package/src/interaction/submit.ts +30 -0
- package/src/interaction/timeline.ts +69 -0
- package/src/interaction/unload.ts +26 -0
- package/src/interaction/visibility.ts +28 -0
- package/src/layout/animation.ts +133 -0
- package/src/layout/custom.ts +43 -0
- package/src/layout/discover.ts +31 -0
- package/src/layout/document.ts +46 -0
- package/src/layout/dom.ts +439 -0
- package/src/layout/encode.ts +154 -0
- package/src/layout/index.ts +42 -0
- package/src/layout/mutation.ts +412 -0
- package/src/layout/node.ts +294 -0
- package/src/layout/offset.ts +19 -0
- package/src/layout/region.ts +151 -0
- package/src/layout/schema.ts +63 -0
- package/src/layout/selector.ts +82 -0
- package/src/layout/style.ts +160 -0
- package/src/layout/target.ts +32 -0
- package/src/layout/traverse.ts +28 -0
- package/src/performance/blank.ts +10 -0
- package/src/performance/encode.ts +31 -0
- package/src/performance/index.ts +12 -0
- package/src/performance/interaction.ts +125 -0
- package/src/performance/navigation.ts +31 -0
- package/src/performance/observer.ts +112 -0
- package/src/queue.ts +33 -0
- package/test/core.test.ts +139 -0
- package/test/helper.ts +167 -0
- package/test/html/core.html +34 -0
- package/test/stub.test.ts +7 -0
- package/test/tsconfig.test.json +6 -0
- package/tsconfig.json +21 -0
- package/tslint.json +33 -0
- package/types/agent.d.ts +39 -0
- package/types/core.d.ts +150 -0
- package/types/data.d.ts +568 -0
- package/types/diagnostic.d.ts +24 -0
- package/types/global.d.ts +30 -0
- package/types/index.d.ts +40 -0
- package/types/interaction.d.ts +174 -0
- package/types/layout.d.ts +277 -0
- 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
|
+
}
|