clarity-js 0.8.12 → 0.8.14
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/build/clarity.extended.js +1 -1
- package/build/clarity.insight.js +1 -1
- package/build/clarity.js +4609 -4762
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +4609 -4762
- package/build/clarity.performance.js +1 -1
- package/package.json +69 -76
- package/rollup.config.ts +88 -84
- package/src/clarity.ts +29 -35
- package/src/core/api.ts +1 -8
- package/src/core/config.ts +2 -2
- package/src/core/event.ts +32 -36
- package/src/core/hash.ts +6 -5
- package/src/core/history.ts +11 -10
- package/src/core/index.ts +11 -21
- package/src/core/measure.ts +5 -9
- package/src/core/report.ts +6 -10
- package/src/core/scrub.ts +27 -30
- package/src/core/task.ts +45 -73
- package/src/core/time.ts +3 -3
- package/src/core/timeout.ts +2 -2
- package/src/core/version.ts +1 -1
- package/src/data/baseline.ts +55 -60
- package/src/data/consent.ts +22 -4
- package/src/data/custom.ts +13 -8
- package/src/data/dimension.ts +7 -11
- package/src/data/encode.ts +38 -36
- package/src/data/envelope.ts +38 -38
- package/src/data/extract.ts +77 -86
- package/src/data/index.ts +9 -11
- package/src/data/limit.ts +1 -1
- package/src/data/metadata.ts +319 -305
- package/src/data/metric.ts +8 -18
- package/src/data/ping.ts +4 -8
- package/src/data/signal.ts +18 -18
- package/src/data/summary.ts +4 -6
- package/src/data/token.ts +8 -10
- package/src/data/upgrade.ts +3 -7
- package/src/data/upload.ts +49 -100
- package/src/data/variable.ts +20 -27
- package/src/diagnostic/encode.ts +2 -2
- package/src/diagnostic/fraud.ts +4 -3
- package/src/diagnostic/internal.ts +5 -11
- package/src/diagnostic/script.ts +8 -12
- package/src/global.ts +1 -1
- package/src/insight/blank.ts +4 -4
- package/src/insight/encode.ts +17 -23
- package/src/insight/snapshot.ts +37 -57
- package/src/interaction/change.ts +6 -9
- package/src/interaction/click.ts +28 -34
- package/src/interaction/clipboard.ts +2 -2
- package/src/interaction/encode.ts +31 -35
- package/src/interaction/input.ts +9 -11
- package/src/interaction/pointer.ts +30 -41
- package/src/interaction/resize.ts +5 -5
- package/src/interaction/scroll.ts +17 -20
- package/src/interaction/selection.ts +8 -12
- package/src/interaction/submit.ts +2 -2
- package/src/interaction/timeline.ts +9 -13
- package/src/interaction/unload.ts +1 -1
- package/src/interaction/visibility.ts +2 -2
- package/src/layout/animation.ts +41 -47
- package/src/layout/discover.ts +5 -5
- package/src/layout/document.ts +19 -31
- package/src/layout/dom.ts +91 -141
- package/src/layout/encode.ts +37 -52
- package/src/layout/mutation.ts +318 -321
- package/src/layout/node.ts +81 -104
- package/src/layout/offset.ts +6 -7
- package/src/layout/region.ts +43 -66
- package/src/layout/schema.ts +8 -15
- package/src/layout/selector.ts +25 -47
- package/src/layout/style.ts +37 -45
- package/src/layout/target.ts +10 -14
- package/src/layout/traverse.ts +11 -17
- package/src/performance/blank.ts +1 -1
- package/src/performance/encode.ts +4 -4
- package/src/performance/interaction.ts +70 -58
- package/src/performance/navigation.ts +2 -2
- package/src/performance/observer.ts +26 -59
- package/src/queue.ts +9 -16
- package/tsconfig.json +1 -1
- package/tslint.json +32 -25
- package/types/core.d.ts +13 -13
- package/types/data.d.ts +48 -32
- package/types/diagnostic.d.ts +1 -1
- package/types/index.d.ts +1 -0
- package/types/interaction.d.ts +4 -5
- package/types/layout.d.ts +21 -36
- package/types/performance.d.ts +5 -6
- package/.lintstagedrc.yml +0 -3
- package/biome.json +0 -43
package/src/layout/node.ts
CHANGED
|
@@ -1,47 +1,46 @@
|
|
|
1
|
-
import { Code, Dimension, Severity } from "@clarity-types/data";
|
|
2
1
|
import { Constant, Source } from "@clarity-types/layout";
|
|
2
|
+
import { Code, Dimension, Severity } from "@clarity-types/data";
|
|
3
|
+
import * as dom from "./dom";
|
|
3
4
|
import * as event from "@src/core/event";
|
|
4
5
|
import * as dimension from "@src/data/dimension";
|
|
5
|
-
import { electron } from "@src/data/metadata";
|
|
6
6
|
import * as internal from "@src/diagnostic/internal";
|
|
7
7
|
import * as interaction from "@src/interaction";
|
|
8
8
|
import * as mutation from "@src/layout/mutation";
|
|
9
9
|
import * as schema from "@src/layout/schema";
|
|
10
10
|
import { checkDocumentStyles } from "@src/layout/style";
|
|
11
|
-
import
|
|
11
|
+
import { electron } from "@src/data/metadata";
|
|
12
12
|
|
|
13
13
|
const IGNORE_ATTRIBUTES = ["title", "alt", "onload", "onfocus", "onerror", "data-drupal-form-submit-last", "aria-label"];
|
|
14
14
|
const newlineRegex = /[\r\n]+/g;
|
|
15
15
|
|
|
16
|
-
export default function (
|
|
17
|
-
let node = inputNode;
|
|
16
|
+
export default function (node: Node, source: Source, timestamp: number): Node {
|
|
18
17
|
let child: Node = null;
|
|
19
18
|
|
|
20
19
|
// Do not track this change if we are attempting to remove a node before discovering it
|
|
21
|
-
if (source === Source.ChildListRemove && dom.has(node) === false) {
|
|
22
|
-
return child;
|
|
23
|
-
}
|
|
20
|
+
if (source === Source.ChildListRemove && dom.has(node) === false) { return child; }
|
|
24
21
|
|
|
25
22
|
// Special handling for text nodes that belong to style nodes
|
|
26
|
-
if (source !== Source.Discover &&
|
|
23
|
+
if (source !== Source.Discover &&
|
|
24
|
+
node.nodeType === Node.TEXT_NODE &&
|
|
25
|
+
node.parentElement &&
|
|
26
|
+
node.parentElement.tagName === "STYLE") {
|
|
27
27
|
node = node.parentNode;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
let add = dom.has(node) === false;
|
|
31
|
+
let call = add ? "add" : "update";
|
|
32
32
|
let parent = node.parentElement ? node.parentElement : null;
|
|
33
|
-
|
|
33
|
+
let insideFrame = node.ownerDocument !== document;
|
|
34
34
|
switch (node.nodeType) {
|
|
35
|
-
case Node.DOCUMENT_TYPE_NODE:
|
|
35
|
+
case Node.DOCUMENT_TYPE_NODE:
|
|
36
36
|
parent = insideFrame && node.parentNode ? dom.iframe(node.parentNode) : parent;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
let docTypePrefix = insideFrame ? Constant.IFramePrefix : Constant.Empty;
|
|
38
|
+
let doctype = node as DocumentType;
|
|
39
|
+
let docName = doctype.name ? doctype.name : Constant.HTML;
|
|
40
|
+
let docAttributes = { name: docName, publicId: doctype.publicId, systemId: doctype.systemId };
|
|
41
|
+
let docData = { tag: docTypePrefix + Constant.DocumentTag, attributes: docAttributes };
|
|
42
42
|
dom[call](node, parent, docData, source);
|
|
43
43
|
break;
|
|
44
|
-
}
|
|
45
44
|
case Node.DOCUMENT_NODE:
|
|
46
45
|
// We check for regions in the beginning when discovering document and
|
|
47
46
|
// later whenever there are new additions or modifications to DOM (mutations)
|
|
@@ -51,20 +50,20 @@ export default function (inputNode: Node, source: Source, timestamp: number): No
|
|
|
51
50
|
checkDocumentStyles(node as Document, timestamp);
|
|
52
51
|
observe(node as Document);
|
|
53
52
|
break;
|
|
54
|
-
case Node.DOCUMENT_FRAGMENT_NODE:
|
|
55
|
-
|
|
53
|
+
case Node.DOCUMENT_FRAGMENT_NODE:
|
|
54
|
+
let shadowRoot = (node as ShadowRoot);
|
|
56
55
|
if (shadowRoot.host) {
|
|
57
56
|
dom.parse(shadowRoot);
|
|
58
|
-
|
|
57
|
+
let type = typeof (shadowRoot.constructor);
|
|
59
58
|
if (type === Constant.Function && shadowRoot.constructor.toString().indexOf(Constant.NativeCode) >= 0) {
|
|
60
59
|
observe(shadowRoot);
|
|
61
|
-
|
|
60
|
+
|
|
62
61
|
// See: https://wicg.github.io/construct-stylesheets/ for more details on adoptedStyleSheets.
|
|
63
62
|
// At the moment, we are only able to capture "open" shadow DOM nodes. If they are closed, they are not accessible.
|
|
64
63
|
// In future we may decide to proxy "attachShadow" call to gain access, but at the moment, we don't want to
|
|
65
64
|
// cause any unintended side effect to the page. We will re-evaluate after we gather more real world data on this.
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
let style = Constant.Empty as string;
|
|
66
|
+
let fragmentData = { tag: Constant.ShadowDomTag, attributes: { style } };
|
|
68
67
|
dom[call](node, shadowRoot.host, fragmentData, source);
|
|
69
68
|
} else {
|
|
70
69
|
// If the browser doesn't support shadow DOM natively, we detect that, and send appropriate tag back.
|
|
@@ -75,100 +74,91 @@ export default function (inputNode: Node, source: Source, timestamp: number): No
|
|
|
75
74
|
checkDocumentStyles(node as Document, timestamp);
|
|
76
75
|
}
|
|
77
76
|
break;
|
|
78
|
-
}
|
|
79
77
|
case Node.TEXT_NODE:
|
|
80
78
|
// In IE11 TEXT_NODE doesn't expose a valid parentElement property. Instead we need to lookup parentNode property.
|
|
81
|
-
parent = parent ? parent :
|
|
79
|
+
parent = parent ? parent : node.parentNode as HTMLElement;
|
|
82
80
|
// Account for this text node only if we are tracking the parent node
|
|
83
81
|
// We do not wish to track text nodes for ignored parent nodes, like script tags
|
|
84
82
|
// Also, we do not track text nodes for STYLE tags
|
|
85
83
|
// The only exception is when we receive a mutation to remove the text node, in that case
|
|
86
84
|
// parent will be null, but we can still process the node by checking it's an update call.
|
|
87
85
|
if (call === "update" || (parent && dom.has(parent) && parent.tagName !== "STYLE" && parent.tagName !== "NOSCRIPT")) {
|
|
88
|
-
|
|
86
|
+
let textData = { tag: Constant.TextTag, value: node.nodeValue };
|
|
89
87
|
dom[call](node, parent, textData, source);
|
|
90
88
|
}
|
|
91
89
|
break;
|
|
92
|
-
case Node.ELEMENT_NODE:
|
|
93
|
-
|
|
90
|
+
case Node.ELEMENT_NODE:
|
|
91
|
+
let element = (node as HTMLElement);
|
|
94
92
|
let tag = element.tagName;
|
|
95
|
-
|
|
93
|
+
let attributes = getAttributes(element);
|
|
96
94
|
// In some cases, external libraries like vue-fragment, can modify parentNode property to not be in sync with the DOM
|
|
97
95
|
// For correctness, we first look at parentElement and if it not present then fall back to using parentNode
|
|
98
|
-
parent = node.parentElement ? node.parentElement : node.parentNode ?
|
|
96
|
+
parent = node.parentElement ? node.parentElement : (node.parentNode ? node.parentNode as HTMLElement : null);
|
|
99
97
|
// If we encounter a node that is part of SVG namespace, prefix the tag with SVG_PREFIX
|
|
100
|
-
if (element.namespaceURI === Constant.SvgNamespace) {
|
|
101
|
-
tag = Constant.SvgPrefix + tag;
|
|
102
|
-
}
|
|
98
|
+
if (element.namespaceURI === Constant.SvgNamespace) { tag = Constant.SvgPrefix + tag; }
|
|
103
99
|
|
|
104
100
|
switch (tag) {
|
|
105
|
-
case "HTML":
|
|
101
|
+
case "HTML":
|
|
106
102
|
parent = insideFrame && parent ? dom.iframe(parent) : parent;
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
let htmlPrefix = insideFrame ? Constant.IFramePrefix : Constant.Empty;
|
|
104
|
+
let htmlData = { tag: htmlPrefix + tag, attributes };
|
|
109
105
|
dom[call](node, parent, htmlData, source);
|
|
110
106
|
break;
|
|
111
|
-
}
|
|
112
107
|
case "SCRIPT":
|
|
113
108
|
if (Constant.Type in attributes && attributes[Constant.Type] === Constant.JsonLD) {
|
|
114
109
|
try {
|
|
115
110
|
schema.ld(JSON.parse((element as HTMLScriptElement).text.replace(newlineRegex, Constant.Empty)));
|
|
116
|
-
} catch {
|
|
117
|
-
/* do nothing */
|
|
118
|
-
}
|
|
111
|
+
} catch { /* do nothing */ }
|
|
119
112
|
}
|
|
120
113
|
break;
|
|
121
|
-
case "NOSCRIPT":
|
|
114
|
+
case "NOSCRIPT":
|
|
122
115
|
// keeping the noscript tag but ignoring its contents. Some HTML markup relies on having these tags
|
|
123
116
|
// to maintain parity with the original css view, but we don't want to execute any noscript in Clarity
|
|
124
|
-
|
|
117
|
+
let noscriptData = { tag, attributes: {}, value: '' };
|
|
125
118
|
dom[call](node, parent, noscriptData, source);
|
|
126
119
|
break;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
case "META":
|
|
121
|
+
var key = (Constant.Property in attributes ?
|
|
122
|
+
Constant.Property :
|
|
123
|
+
(Constant.Name in attributes ? Constant.Name : null));
|
|
130
124
|
if (key && Constant.Content in attributes) {
|
|
131
|
-
|
|
132
|
-
switch
|
|
125
|
+
let content = attributes[Constant.Content]
|
|
126
|
+
switch(attributes[key]) {
|
|
133
127
|
case Constant.ogTitle:
|
|
134
|
-
dimension.log(Dimension.MetaTitle, content)
|
|
128
|
+
dimension.log(Dimension.MetaTitle, content)
|
|
135
129
|
break;
|
|
136
130
|
case Constant.ogType:
|
|
137
|
-
dimension.log(Dimension.MetaType, content)
|
|
131
|
+
dimension.log(Dimension.MetaType, content)
|
|
138
132
|
break;
|
|
139
133
|
case Constant.Generator:
|
|
140
|
-
dimension.log(Dimension.Generator, content)
|
|
134
|
+
dimension.log(Dimension.Generator, content)
|
|
141
135
|
break;
|
|
142
136
|
}
|
|
143
137
|
}
|
|
144
138
|
break;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
head.attributes[Constant.Base] = `${l.protocol}//${l.host}${l.pathname}`;
|
|
139
|
+
case "HEAD":
|
|
140
|
+
let head = { tag, attributes };
|
|
141
|
+
let l = insideFrame && node.ownerDocument?.location ? node.ownerDocument.location : location;
|
|
142
|
+
head.attributes[Constant.Base] = l.protocol + "//" + l.host + l.pathname;
|
|
150
143
|
dom[call](node, parent, head, source);
|
|
151
144
|
break;
|
|
152
|
-
|
|
153
|
-
case "BASE": {
|
|
145
|
+
case "BASE":
|
|
154
146
|
// Override the auto detected base path to explicit value specified in this tag
|
|
155
|
-
|
|
147
|
+
let baseHead = dom.get(node.parentElement);
|
|
156
148
|
if (baseHead) {
|
|
157
149
|
// We create "a" element so we can generate protocol and hostname for relative paths like "/path/"
|
|
158
|
-
|
|
159
|
-
a.href = attributes
|
|
160
|
-
baseHead.data.attributes[Constant.Base] =
|
|
150
|
+
let a = document.createElement("a");
|
|
151
|
+
a.href = attributes["href"];
|
|
152
|
+
baseHead.data.attributes[Constant.Base] = a.protocol + "//" + a.host + a.pathname;
|
|
161
153
|
}
|
|
162
154
|
break;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const styleData = { tag, attributes, value: getStyleValue(element as HTMLStyleElement) };
|
|
155
|
+
case "STYLE":
|
|
156
|
+
let styleData = { tag, attributes, value: getStyleValue(element as HTMLStyleElement) };
|
|
166
157
|
dom[call](node, parent, styleData, source);
|
|
167
158
|
break;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const frameData = { tag, attributes };
|
|
159
|
+
case "IFRAME":
|
|
160
|
+
let iframe = node as HTMLIFrameElement;
|
|
161
|
+
let frameData = { tag, attributes };
|
|
172
162
|
if (dom.sameorigin(iframe)) {
|
|
173
163
|
mutation.monitor(iframe);
|
|
174
164
|
frameData.attributes[Constant.SameOrigin] = "true";
|
|
@@ -181,15 +171,14 @@ export default function (inputNode: Node, source: Source, timestamp: number): No
|
|
|
181
171
|
}
|
|
182
172
|
dom[call](node, parent, frameData, source);
|
|
183
173
|
break;
|
|
184
|
-
|
|
185
|
-
case "LINK": {
|
|
174
|
+
case "LINK":
|
|
186
175
|
// electron stylesheets reference the local file system - translating those
|
|
187
176
|
// to inline styles so playback can work
|
|
188
|
-
if (electron && attributes
|
|
189
|
-
for (
|
|
190
|
-
|
|
191
|
-
if (currentStyleSheet.ownerNode
|
|
192
|
-
|
|
177
|
+
if (electron && attributes['rel'] === Constant.StyleSheet) {
|
|
178
|
+
for (var styleSheetIndex in Object.keys(document.styleSheets)) {
|
|
179
|
+
var currentStyleSheet = document.styleSheets[styleSheetIndex];
|
|
180
|
+
if (currentStyleSheet.ownerNode == element) {
|
|
181
|
+
let syntheticStyleData = { tag: "STYLE", attributes, value: getCssRules(currentStyleSheet) };
|
|
193
182
|
dom[call](node, parent, syntheticStyleData, source);
|
|
194
183
|
break;
|
|
195
184
|
}
|
|
@@ -197,32 +186,26 @@ export default function (inputNode: Node, source: Source, timestamp: number): No
|
|
|
197
186
|
break;
|
|
198
187
|
}
|
|
199
188
|
// for links that aren't electron style sheets we can process them normally
|
|
200
|
-
|
|
189
|
+
let linkData = { tag, attributes };
|
|
201
190
|
dom[call](node, parent, linkData, source);
|
|
202
191
|
break;
|
|
203
|
-
}
|
|
204
192
|
case "VIDEO":
|
|
205
193
|
case "AUDIO":
|
|
206
|
-
case "SOURCE":
|
|
194
|
+
case "SOURCE":
|
|
207
195
|
// Ignoring any base64 src attribute for media elements to prevent big unused tokens to be sent and shock the network
|
|
208
196
|
if (Constant.Src in attributes && attributes[Constant.Src].startsWith("data:")) {
|
|
209
197
|
attributes[Constant.Src] = "";
|
|
210
198
|
}
|
|
211
|
-
|
|
199
|
+
let mediaTag = { tag, attributes };
|
|
212
200
|
dom[call](node, parent, mediaTag, source);
|
|
213
201
|
break;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (element.shadowRoot) {
|
|
218
|
-
child = element.shadowRoot;
|
|
219
|
-
}
|
|
202
|
+
default:
|
|
203
|
+
let data = { tag, attributes };
|
|
204
|
+
if (element.shadowRoot) { child = element.shadowRoot; }
|
|
220
205
|
dom[call](node, parent, data, source);
|
|
221
206
|
break;
|
|
222
|
-
}
|
|
223
207
|
}
|
|
224
208
|
break;
|
|
225
|
-
}
|
|
226
209
|
default:
|
|
227
210
|
break;
|
|
228
211
|
}
|
|
@@ -230,9 +213,7 @@ export default function (inputNode: Node, source: Source, timestamp: number): No
|
|
|
230
213
|
}
|
|
231
214
|
|
|
232
215
|
function observe(root: Document | ShadowRoot): void {
|
|
233
|
-
if (dom.has(root) || event.has(root)) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
216
|
+
if (dom.has(root) || event.has(root)) { return; }
|
|
236
217
|
mutation.observe(root); // Observe mutations for this root node
|
|
237
218
|
interaction.observe(root); // Observe interactions for this root node
|
|
238
219
|
}
|
|
@@ -247,13 +228,13 @@ export function removeObserver(root: HTMLIFrameElement): void {
|
|
|
247
228
|
// For iframes, scroll event is observed on content window and this needs to be removed as well
|
|
248
229
|
event.unbind(win);
|
|
249
230
|
}
|
|
250
|
-
|
|
231
|
+
|
|
251
232
|
if (doc) {
|
|
252
233
|
// When an iframe is removed, we should also remove all listeners attached to its document
|
|
253
234
|
// to avoid memory leaks.
|
|
254
235
|
event.unbind(doc);
|
|
255
236
|
mutation.disconnect(doc);
|
|
256
|
-
|
|
237
|
+
|
|
257
238
|
// Remove iframe and content document from maps tracking them
|
|
258
239
|
dom.removeIFrame(root, doc);
|
|
259
240
|
}
|
|
@@ -265,7 +246,7 @@ function getStyleValue(style: HTMLStyleElement): string {
|
|
|
265
246
|
// Additionally, check if style node has an id - if so it's at a high risk to have experienced dynamic
|
|
266
247
|
// style updates which would make the textContent out of date with its true style contribution.
|
|
267
248
|
let value = style.textContent ? style.textContent.trim() : Constant.Empty;
|
|
268
|
-
|
|
249
|
+
let dataset = style.dataset ? Object.keys(style.dataset).length : 0;
|
|
269
250
|
if (value.length === 0 || dataset > 0 || style.id.length > 0) {
|
|
270
251
|
value = getCssRules(style.sheet as CSSStyleSheet);
|
|
271
252
|
}
|
|
@@ -276,13 +257,9 @@ export function getCssRules(sheet: CSSStyleSheet): string {
|
|
|
276
257
|
let value = Constant.Empty as string;
|
|
277
258
|
let cssRules = null;
|
|
278
259
|
// Firefox throws a SecurityError when trying to access cssRules of a stylesheet from a different domain
|
|
279
|
-
try {
|
|
280
|
-
cssRules = sheet ? sheet.cssRules : [];
|
|
281
|
-
} catch (e) {
|
|
260
|
+
try { cssRules = sheet ? sheet.cssRules : []; } catch (e) {
|
|
282
261
|
internal.log(Code.CssRules, Severity.Warning, e ? e.name : null);
|
|
283
|
-
if (e && e.name !== "SecurityError") {
|
|
284
|
-
throw e;
|
|
285
|
-
}
|
|
262
|
+
if (e && e.name !== "SecurityError") { throw e; }
|
|
286
263
|
}
|
|
287
264
|
|
|
288
265
|
if (cssRules !== null) {
|
|
@@ -295,11 +272,11 @@ export function getCssRules(sheet: CSSStyleSheet): string {
|
|
|
295
272
|
}
|
|
296
273
|
|
|
297
274
|
function getAttributes(element: HTMLElement): { [key: string]: string } {
|
|
298
|
-
|
|
299
|
-
|
|
275
|
+
let output = {};
|
|
276
|
+
let attributes = element.attributes;
|
|
300
277
|
if (attributes && attributes.length > 0) {
|
|
301
278
|
for (let i = 0; i < attributes.length; i++) {
|
|
302
|
-
|
|
279
|
+
let name = attributes[i].name;
|
|
303
280
|
if (IGNORE_ATTRIBUTES.indexOf(name) < 0) {
|
|
304
281
|
output[name] = attributes[i].value;
|
|
305
282
|
}
|
package/src/layout/offset.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { OffsetDistance } from "@clarity-types/core";
|
|
2
2
|
import { iframe } from "@src/layout/dom";
|
|
3
3
|
|
|
4
|
-
export function offset(
|
|
5
|
-
let
|
|
6
|
-
const output: OffsetDistance = { x: 0, y: 0 };
|
|
4
|
+
export function offset(element: HTMLElement): OffsetDistance {
|
|
5
|
+
let output: OffsetDistance = { x: 0, y: 0 };
|
|
7
6
|
|
|
8
7
|
// Walk up the chain to ensure we compute offset distance correctly
|
|
9
8
|
// In case where we may have nested IFRAMEs, we keep walking up until we get to the top most parent page
|
|
10
|
-
if (element
|
|
9
|
+
if (element && element.offsetParent) {
|
|
11
10
|
do {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
let parent = element.offsetParent as HTMLElement;
|
|
12
|
+
let frame = parent === null ? iframe(element.ownerDocument) : null;
|
|
14
13
|
output.x += element.offsetLeft;
|
|
15
14
|
output.y += element.offsetTop;
|
|
16
15
|
element = frame ? frame : parent;
|
package/src/layout/region.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Event, Setting } from "@clarity-types/data";
|
|
2
|
-
import { InteractionState,
|
|
2
|
+
import { InteractionState, RegionData, RegionState, RegionQueue, RegionVisibility } from "@clarity-types/layout";
|
|
3
3
|
import { FunctionNames } from "@clarity-types/performance";
|
|
4
4
|
import { time } from "@src/core/time";
|
|
5
5
|
import * as dom from "@src/layout/dom";
|
|
@@ -18,22 +18,20 @@ export function start(): void {
|
|
|
18
18
|
regionMap = new WeakMap();
|
|
19
19
|
regions = {};
|
|
20
20
|
queue = [];
|
|
21
|
-
watch =
|
|
21
|
+
watch = window["IntersectionObserver"] ? true : false;
|
|
22
|
+
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function observe(node: Node, name: string): void {
|
|
25
26
|
if (regionMap.has(node) === false) {
|
|
26
27
|
regionMap.set(node, name);
|
|
27
|
-
observer =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
threshold: [0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],
|
|
35
|
-
})
|
|
36
|
-
: observer;
|
|
28
|
+
observer = observer === null && watch ? new IntersectionObserver(handler, {
|
|
29
|
+
// Get notified as intersection continues to change
|
|
30
|
+
// This allows us to process regions that get partially hidden during the lifetime of the page
|
|
31
|
+
// See: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#creating_an_intersection_observer
|
|
32
|
+
// By default, intersection observers only fire an event when even a single pixel is visible and not thereafter.
|
|
33
|
+
threshold: [0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
|
|
34
|
+
}) : observer;
|
|
37
35
|
if (observer && node && node.nodeType === Node.ELEMENT_NODE) {
|
|
38
36
|
observer.observe(node as Element);
|
|
39
37
|
}
|
|
@@ -44,25 +42,18 @@ export function exists(node: Node): boolean {
|
|
|
44
42
|
// Check if regionMap is not null before looking up a node
|
|
45
43
|
// Since, dom module stops after region module, it's possible that we may set regionMap to be null
|
|
46
44
|
// and still attempt to call exists on a late coming DOM mutation (or addition), effectively causing a script error
|
|
47
|
-
return regionMap
|
|
45
|
+
return regionMap && regionMap.has(node);
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
export function track(id: number, event: Event): void {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
? regions[id]
|
|
55
|
-
: { id, visibility: RegionVisibility.Rendered, interaction: InteractionState.None, name: regionMap.get(node) };
|
|
56
|
-
|
|
49
|
+
let node = dom.getNode(id);
|
|
50
|
+
let data = id in regions ? regions[id] : { id, visibility: RegionVisibility.Rendered, interaction: InteractionState.None, name: regionMap.get(node) };
|
|
51
|
+
|
|
57
52
|
// Determine the interaction state based on incoming event
|
|
58
53
|
let interaction = InteractionState.None;
|
|
59
54
|
switch (event) {
|
|
60
|
-
case Event.Click:
|
|
61
|
-
|
|
62
|
-
break;
|
|
63
|
-
case Event.Input:
|
|
64
|
-
interaction = InteractionState.Input;
|
|
65
|
-
break;
|
|
55
|
+
case Event.Click: interaction = InteractionState.Clicked; break;
|
|
56
|
+
case Event.Input: interaction = InteractionState.Input; break;
|
|
66
57
|
}
|
|
67
58
|
// Process updates to this region, if applicable
|
|
68
59
|
process(node, data, interaction, data.visibility);
|
|
@@ -73,73 +64,59 @@ export function compute(): void {
|
|
|
73
64
|
// Process any regions where we couldn't resolve an "id" for at the time of last intersection observer event
|
|
74
65
|
// This could happen in cases where elements are not yet processed by Clarity's virtual DOM but browser reports a change, regardless.
|
|
75
66
|
// For those cases we add them to the queue and re-process them below
|
|
76
|
-
|
|
77
|
-
for (
|
|
78
|
-
|
|
67
|
+
let q = [];
|
|
68
|
+
for (let r of queue) {
|
|
69
|
+
let id = dom.getId(r.node);
|
|
79
70
|
if (id) {
|
|
80
71
|
r.state.data.id = id;
|
|
81
72
|
regions[id] = r.state.data;
|
|
82
73
|
state.push(r.state);
|
|
83
|
-
} else {
|
|
84
|
-
q.push(r);
|
|
85
|
-
}
|
|
74
|
+
} else { q.push(r); }
|
|
86
75
|
}
|
|
87
76
|
queue = q;
|
|
88
77
|
|
|
89
78
|
// Schedule encode only when we have at least one valid data entry
|
|
90
|
-
if (state.length > 0) {
|
|
91
|
-
encode(Event.Region);
|
|
92
|
-
}
|
|
79
|
+
if (state.length > 0) { encode(Event.Region); }
|
|
93
80
|
}
|
|
94
81
|
|
|
95
82
|
function handler(entries: IntersectionObserverEntry[]): void {
|
|
96
|
-
for (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
83
|
+
for (let entry of entries) {
|
|
84
|
+
let target = entry.target;
|
|
85
|
+
let rect = entry.boundingClientRect;
|
|
86
|
+
let overlap = entry.intersectionRect;
|
|
87
|
+
let viewport = entry.rootBounds;
|
|
101
88
|
// Only capture regions that have non-zero width or height to avoid tracking and sending regions
|
|
102
89
|
// that cannot ever be seen by the user. In some cases, websites will have a multiple copy of the same region
|
|
103
90
|
// like search box - one for desktop, and another for mobile. In those cases, CSS media queries determine which one should be visible.
|
|
104
91
|
// Also, if these regions ever become non-zero width or height (through AJAX, user action or orientation change) - we will automatically start monitoring them from that point onwards
|
|
105
92
|
if (regionMap.has(target) && rect.width + rect.height > 0 && viewport && viewport.width > 0 && viewport.height > 0) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
? regions[id]
|
|
110
|
-
: { id, name: regionMap.get(target), interaction: InteractionState.None, visibility: RegionVisibility.Rendered };
|
|
111
|
-
|
|
93
|
+
let id = target ? dom.getId(target) : null;
|
|
94
|
+
let data = id in regions ? regions[id] : { id, name: regionMap.get(target), interaction: InteractionState.None, visibility: RegionVisibility.Rendered };
|
|
95
|
+
|
|
112
96
|
// For regions that have relatively smaller area, we look at intersection ratio and see the overlap relative to element's area
|
|
113
97
|
// However, for larger regions, area of regions could be bigger than viewport and therefore comparison is relative to visible area
|
|
114
|
-
|
|
115
|
-
|
|
98
|
+
let viewportRatio = overlap ? (overlap.width * overlap.height * 1.0) / (viewport.width * viewport.height) : 0;
|
|
99
|
+
let visible = viewportRatio > Setting.ViewportIntersectionRatio || entry.intersectionRatio > Setting.IntersectionRatio;
|
|
116
100
|
// If an element is either visible or was visible and has been scrolled to the end
|
|
117
|
-
// i.e. Scrolled to end is determined by if the starting position of the element + the window height is more than the total element height.
|
|
101
|
+
// i.e. Scrolled to end is determined by if the starting position of the element + the window height is more than the total element height.
|
|
118
102
|
// starting position is relative to the viewport - so Intersection observer returns a negative value for rect.top to indicate that the element top is above the viewport
|
|
119
|
-
|
|
120
|
-
(visible || data.visibility === RegionVisibility.Visible) && Math.abs(rect.top) + viewport.height > rect.height;
|
|
103
|
+
let scrolledToEnd = (visible || data.visibility == RegionVisibility.Visible) && Math.abs(rect.top) + viewport.height > rect.height;
|
|
121
104
|
// Process updates to this region, if applicable
|
|
122
|
-
process(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
scrolledToEnd ? RegionVisibility.ScrolledToEnd : visible ? RegionVisibility.Visible : RegionVisibility.Rendered,
|
|
127
|
-
);
|
|
105
|
+
process(target, data, data.interaction,
|
|
106
|
+
(scrolledToEnd ?
|
|
107
|
+
RegionVisibility.ScrolledToEnd :
|
|
108
|
+
(visible ? RegionVisibility.Visible : RegionVisibility.Rendered)));
|
|
128
109
|
|
|
129
110
|
// Stop observing this element now that we have already received scrolled signal
|
|
130
|
-
if (data.visibility >= RegionVisibility.ScrolledToEnd && observer) {
|
|
131
|
-
observer.unobserve(target);
|
|
132
|
-
}
|
|
111
|
+
if (data.visibility >= RegionVisibility.ScrolledToEnd && observer) { observer.unobserve(target); }
|
|
133
112
|
}
|
|
134
113
|
}
|
|
135
|
-
if (state.length > 0) {
|
|
136
|
-
encode(Event.Region);
|
|
137
|
-
}
|
|
114
|
+
if (state.length > 0) { encode(Event.Region); }
|
|
138
115
|
}
|
|
139
116
|
|
|
140
117
|
function process(n: Node, d: RegionData, s: InteractionState, v: RegionVisibility): void {
|
|
141
118
|
// Check if received a state that supersedes existing state
|
|
142
|
-
|
|
119
|
+
let updated = s > d.interaction || v > d.visibility;
|
|
143
120
|
d.interaction = s > d.interaction ? s : d.interaction;
|
|
144
121
|
d.visibility = v > d.visibility ? v : d.visibility;
|
|
145
122
|
// If the corresponding node is already discovered, update the internal state
|
|
@@ -151,16 +128,16 @@ function process(n: Node, d: RegionData, s: InteractionState, v: RegionVisibilit
|
|
|
151
128
|
}
|
|
152
129
|
} else {
|
|
153
130
|
// Get the time before adding to queue to ensure accurate event time
|
|
154
|
-
queue.push({
|
|
131
|
+
queue.push({node: n, state: clone(d)});
|
|
155
132
|
}
|
|
156
133
|
}
|
|
157
134
|
|
|
158
135
|
function clone(r: RegionData): RegionState {
|
|
159
|
-
return { time: time(), data: { id: r.id, interaction: r.interaction, visibility: r.visibility, name: r.name }
|
|
136
|
+
return { time: time(), data: { id: r.id, interaction: r.interaction, visibility: r.visibility, name: r.name }};
|
|
160
137
|
}
|
|
161
138
|
|
|
162
139
|
export function reset(): void {
|
|
163
|
-
state = [];
|
|
140
|
+
state = [];
|
|
164
141
|
}
|
|
165
142
|
|
|
166
143
|
export function stop(): void {
|
package/src/layout/schema.ts
CHANGED
|
@@ -6,9 +6,8 @@ import * as metric from "@src/data/metric";
|
|
|
6
6
|
const digitsRegex = /[^0-9\.]/g;
|
|
7
7
|
|
|
8
8
|
/* JSON+LD (Linked Data) Recursive Parser */
|
|
9
|
-
// biome-ignore lint/suspicious/noExplicitAny: specifically parsing json with any type
|
|
10
9
|
export function ld(json: any): void {
|
|
11
|
-
for (
|
|
10
|
+
for (let key of Object.keys(json)) {
|
|
12
11
|
let value = json[key];
|
|
13
12
|
if (key === JsonLD.Type && typeof value === "string") {
|
|
14
13
|
value = value.toLowerCase();
|
|
@@ -18,16 +17,14 @@ export function ld(json: any): void {
|
|
|
18
17
|
case JsonLD.Article:
|
|
19
18
|
case JsonLD.Recipe:
|
|
20
19
|
dimension.log(Dimension.SchemaType, json[key]);
|
|
21
|
-
dimension.log(Dimension.AuthorName,
|
|
22
|
-
dimension.log(Dimension.Headline,
|
|
20
|
+
dimension.log(Dimension.AuthorName, json[JsonLD.Creator]);
|
|
21
|
+
dimension.log(Dimension.Headline, json[JsonLD.Headline]);
|
|
23
22
|
break;
|
|
24
23
|
case JsonLD.Product:
|
|
25
24
|
dimension.log(Dimension.SchemaType, json[key]);
|
|
26
25
|
dimension.log(Dimension.ProductName, json[JsonLD.Name]);
|
|
27
26
|
dimension.log(Dimension.ProductSku, json[JsonLD.Sku]);
|
|
28
|
-
if (json[JsonLD.Brand]) {
|
|
29
|
-
dimension.log(Dimension.ProductBrand, json[JsonLD.Brand][JsonLD.Name]);
|
|
30
|
-
}
|
|
27
|
+
if (json[JsonLD.Brand]) { dimension.log(Dimension.ProductBrand, json[JsonLD.Brand][JsonLD.Name]); }
|
|
31
28
|
break;
|
|
32
29
|
case JsonLD.AggregateRating:
|
|
33
30
|
if (json[JsonLD.RatingValue]) {
|
|
@@ -51,19 +48,15 @@ export function ld(json: any): void {
|
|
|
51
48
|
}
|
|
52
49
|
}
|
|
53
50
|
// Continue parsing nested objects
|
|
54
|
-
if (value !== null && typeof
|
|
55
|
-
ld(value);
|
|
56
|
-
}
|
|
51
|
+
if (value !== null && typeof(value) === Constant.Object) { ld(value); }
|
|
57
52
|
}
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
function num(input: string | number, scale = 1): number {
|
|
55
|
+
function num(input: string | number, scale: number = 1): number {
|
|
61
56
|
if (input !== null) {
|
|
62
57
|
switch (typeof input) {
|
|
63
|
-
case Constant.Number:
|
|
64
|
-
|
|
65
|
-
case Constant.String:
|
|
66
|
-
return Math.round(Number.parseFloat((input as string).replace(digitsRegex, Constant.Empty)) * scale);
|
|
58
|
+
case Constant.Number: return Math.round((input as number) * scale);
|
|
59
|
+
case Constant.String: return Math.round(parseFloat((input as string).replace(digitsRegex, Constant.Empty)) * scale);
|
|
67
60
|
}
|
|
68
61
|
}
|
|
69
62
|
return null;
|