clarity-js 0.7.67 → 0.7.69
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 +87 -28
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +87 -28
- package/build/clarity.performance.js +1 -1
- package/package.json +1 -1
- package/src/core/event.ts +14 -10
- package/src/core/time.ts +2 -2
- package/src/core/version.ts +1 -1
- package/src/data/baseline.ts +32 -0
- package/src/data/encode.ts +6 -0
- package/src/data/metadata.ts +1 -1
- package/src/interaction/click.ts +12 -6
- package/src/interaction/encode.ts +2 -0
- package/src/interaction/timeline.ts +1 -1
- package/src/interaction/unload.ts +3 -3
- package/src/layout/mutation.ts +158 -94
- package/src/layout/style.ts +8 -7
- package/types/core.d.ts +4 -1
- package/types/data.d.ts +6 -0
- package/types/interaction.d.ts +8 -0
package/src/interaction/click.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BooleanFlag, Constant, Event, Setting } from "@clarity-types/data";
|
|
2
|
-
import { BrowsingContext, ClickState } from "@clarity-types/interaction";
|
|
2
|
+
import { BrowsingContext, ClickState, TextInfo } from "@clarity-types/interaction";
|
|
3
3
|
import { Box } from "@clarity-types/layout";
|
|
4
4
|
import { FunctionNames } from "@clarity-types/performance";
|
|
5
5
|
import { bind } from "@src/core/event";
|
|
@@ -55,6 +55,7 @@ function handler(event: Event, root: Node, evt: MouseEvent): void {
|
|
|
55
55
|
|
|
56
56
|
// Check for null values before processing this event
|
|
57
57
|
if (x !== null && y !== null) {
|
|
58
|
+
const textInfo = text(t);
|
|
58
59
|
state.push({
|
|
59
60
|
time: time(evt), event, data: {
|
|
60
61
|
target: t,
|
|
@@ -65,10 +66,11 @@ function handler(event: Event, root: Node, evt: MouseEvent): void {
|
|
|
65
66
|
button: evt.button,
|
|
66
67
|
reaction: reaction(t),
|
|
67
68
|
context: context(a),
|
|
68
|
-
text: text
|
|
69
|
+
text: textInfo.text,
|
|
69
70
|
link: a ? a.href : null,
|
|
70
71
|
hash: null,
|
|
71
|
-
trust: evt.isTrusted ? BooleanFlag.True : BooleanFlag.False
|
|
72
|
+
trust: evt.isTrusted ? BooleanFlag.True : BooleanFlag.False,
|
|
73
|
+
isFullText: textInfo.isFullText,
|
|
72
74
|
}
|
|
73
75
|
});
|
|
74
76
|
schedule(encode.bind(this, event));
|
|
@@ -88,19 +90,23 @@ function link(node: Node): HTMLAnchorElement {
|
|
|
88
90
|
return null;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
function text(element: Node):
|
|
93
|
+
function text(element: Node): TextInfo {
|
|
92
94
|
let output = null;
|
|
95
|
+
let isFullText = false;
|
|
93
96
|
if (element) {
|
|
94
97
|
// Grab text using "textContent" for most HTMLElements, however, use "value" for HTMLInputElements and "alt" for HTMLImageElement.
|
|
95
98
|
let t = element.textContent || String((element as HTMLInputElement).value || '') || (element as HTMLImageElement).alt;
|
|
96
99
|
if (t) {
|
|
97
100
|
// Replace multiple occurrence of space characters with a single white space
|
|
98
101
|
// Also, trim any spaces at the beginning or at the end of string
|
|
102
|
+
const trimmedText = t.replace(/\s+/g, Constant.Space).trim();
|
|
99
103
|
// Finally, send only first few characters as specified by the Setting
|
|
100
|
-
output =
|
|
104
|
+
output = trimmedText.substring(0, Setting.ClickText);
|
|
105
|
+
isFullText = output.length === trimmedText.length;
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
|
-
|
|
108
|
+
|
|
109
|
+
return { text: output, isFullText: isFullText ? BooleanFlag.True : BooleanFlag.False };
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
function reaction(element: Node): BooleanFlag {
|
|
@@ -67,6 +67,7 @@ export default async function (type: Event, ts: number = null): Promise<void> {
|
|
|
67
67
|
tokens.push(scrub.url(entry.data.link));
|
|
68
68
|
tokens.push(cHash);
|
|
69
69
|
tokens.push(entry.data.trust);
|
|
70
|
+
tokens.push(entry.data.isFullText);
|
|
70
71
|
queue(tokens);
|
|
71
72
|
timeline.track(entry.time, entry.event, cHash, entry.data.x, entry.data.y, entry.data.reaction, entry.data.context);
|
|
72
73
|
}
|
|
@@ -95,6 +96,7 @@ export default async function (type: Event, ts: number = null): Promise<void> {
|
|
|
95
96
|
case Event.Unload:
|
|
96
97
|
let u = unload.data;
|
|
97
98
|
tokens.push(u.name);
|
|
99
|
+
tokens.push(u.persisted);
|
|
98
100
|
unload.reset();
|
|
99
101
|
queue(tokens);
|
|
100
102
|
break;
|
|
@@ -39,7 +39,7 @@ export function track(time: number,
|
|
|
39
39
|
// Since timeline only keeps the data for configured time, we still want to continue tracking these values
|
|
40
40
|
// as part of the baseline. For instance, in a scenario where last scroll happened 5s ago.
|
|
41
41
|
// We would still need to capture the last scroll position as part of the baseline event, even when timeline will be empty.
|
|
42
|
-
baseline.track(event, x, y);
|
|
42
|
+
baseline.track(event, x, y, time);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function compute(): void {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Event } from "@clarity-types/data";
|
|
1
|
+
import { BooleanFlag, Event } from "@clarity-types/data";
|
|
2
2
|
import { UnloadData } from "@clarity-types/interaction";
|
|
3
3
|
import { FunctionNames } from "@clarity-types/performance";
|
|
4
4
|
import * as clarity from "@src/clarity";
|
|
@@ -12,9 +12,9 @@ export function start(): void {
|
|
|
12
12
|
bind(window, "pagehide", recompute);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function recompute(evt:
|
|
15
|
+
function recompute(evt: PageTransitionEvent): void {
|
|
16
16
|
recompute.dn = FunctionNames.UnloadRecompute;
|
|
17
|
-
data = { name: evt.type };
|
|
17
|
+
data = { name: evt.type, persisted: evt.persisted ? BooleanFlag.True : BooleanFlag.False };
|
|
18
18
|
encode(Event.Unload, time(evt));
|
|
19
19
|
clarity.stop();
|
|
20
20
|
}
|
package/src/layout/mutation.ts
CHANGED
|
@@ -26,7 +26,7 @@ let mutations: MutationQueue[] = [];
|
|
|
26
26
|
let throttledMutations: { [key: number]: MutationRecordWithTime } = {};
|
|
27
27
|
let insertRule: (rule: string, index?: number) => number = null;
|
|
28
28
|
let deleteRule: (index?: number) => void = null;
|
|
29
|
-
let attachShadow: (init: ShadowRootInit)
|
|
29
|
+
let attachShadow: (init: ShadowRootInit) => ShadowRoot = null;
|
|
30
30
|
let mediaInsertRule: (rule: string, index?: number) => number = null;
|
|
31
31
|
let mediaDeleteRule: (index?: number) => void = null;
|
|
32
32
|
let queue: Node[] = [];
|
|
@@ -36,63 +36,79 @@ let activePeriod = null;
|
|
|
36
36
|
let history: MutationHistory = {};
|
|
37
37
|
let criticalPeriod = null;
|
|
38
38
|
|
|
39
|
+
// We ignore mutations if these attributes are updated
|
|
40
|
+
const IGNORED_ATTRIBUTES = ["data-google-query-id", "data-load-complete", "data-google-container-id"];
|
|
41
|
+
|
|
39
42
|
export function start(): void {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Some popular open source libraries, like styled-components, optimize performance
|
|
49
|
-
// by injecting CSS using insertRule API vs. appending text node. A side effect of
|
|
50
|
-
// using javascript API is that it doesn't trigger DOM mutation and therefore we
|
|
51
|
-
// need to override the insertRule API and listen for changes manually.
|
|
52
|
-
if (insertRule === null) {
|
|
53
|
-
insertRule = CSSStyleSheet.prototype.insertRule;
|
|
54
|
-
CSSStyleSheet.prototype.insertRule = function(): number {
|
|
55
|
-
if (core.active()) { schedule(this.ownerNode); }
|
|
56
|
-
return insertRule.apply(this, arguments);
|
|
57
|
-
};
|
|
58
|
-
}
|
|
43
|
+
start.dn = FunctionNames.MutationStart;
|
|
44
|
+
observers = [];
|
|
45
|
+
queue = [];
|
|
46
|
+
timeout = null;
|
|
47
|
+
activePeriod = 0;
|
|
48
|
+
history = {};
|
|
49
|
+
criticalPeriod = 0;
|
|
59
50
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
// Some popular open source libraries, like styled-components, optimize performance
|
|
52
|
+
// by injecting CSS using insertRule API vs. appending text node. A side effect of
|
|
53
|
+
// using javascript API is that it doesn't trigger DOM mutation and therefore we
|
|
54
|
+
// need to override the insertRule API and listen for changes manually.
|
|
55
|
+
if (insertRule === null) {
|
|
56
|
+
insertRule = CSSStyleSheet.prototype.insertRule;
|
|
57
|
+
CSSStyleSheet.prototype.insertRule = function (): number {
|
|
58
|
+
if (core.active()) {
|
|
59
|
+
schedule(this.ownerNode);
|
|
60
|
+
}
|
|
61
|
+
return insertRule.apply(this, arguments);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
65
|
+
if ("CSSMediaRule" in window && mediaInsertRule === null) {
|
|
66
|
+
mediaInsertRule = CSSMediaRule.prototype.insertRule;
|
|
67
|
+
CSSMediaRule.prototype.insertRule = function (): number {
|
|
68
|
+
if (core.active()) {
|
|
69
|
+
schedule(this.parentStyleSheet.ownerNode);
|
|
70
|
+
}
|
|
71
|
+
return mediaInsertRule.apply(this, arguments);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (deleteRule === null) {
|
|
76
|
+
deleteRule = CSSStyleSheet.prototype.deleteRule;
|
|
77
|
+
CSSStyleSheet.prototype.deleteRule = function (): void {
|
|
78
|
+
if (core.active()) {
|
|
79
|
+
schedule(this.ownerNode);
|
|
80
|
+
}
|
|
81
|
+
return deleteRule.apply(this, arguments);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if ("CSSMediaRule" in window && mediaDeleteRule === null) {
|
|
86
|
+
mediaDeleteRule = CSSMediaRule.prototype.deleteRule;
|
|
87
|
+
CSSMediaRule.prototype.deleteRule = function (): void {
|
|
88
|
+
if (core.active()) {
|
|
89
|
+
schedule(this.parentStyleSheet.ownerNode);
|
|
90
|
+
}
|
|
91
|
+
return mediaDeleteRule.apply(this, arguments);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
95
|
+
// Add a hook to attachShadow API calls
|
|
96
|
+
// In case we are unable to add a hook and browser throws an exception,
|
|
97
|
+
// reset attachShadow variable and resume processing like before
|
|
98
|
+
if (attachShadow === null) {
|
|
99
|
+
attachShadow = Element.prototype.attachShadow;
|
|
100
|
+
try {
|
|
101
|
+
Element.prototype.attachShadow = function (): ShadowRoot {
|
|
102
|
+
if (core.active()) {
|
|
103
|
+
return schedule(attachShadow.apply(this, arguments)) as ShadowRoot;
|
|
104
|
+
} else {
|
|
105
|
+
return attachShadow.apply(this, arguments);
|
|
106
|
+
}
|
|
81
107
|
};
|
|
108
|
+
} catch {
|
|
109
|
+
attachShadow = null;
|
|
82
110
|
}
|
|
83
|
-
|
|
84
|
-
// Add a hook to attachShadow API calls
|
|
85
|
-
// In case we are unable to add a hook and browser throws an exception,
|
|
86
|
-
// reset attachShadow variable and resume processing like before
|
|
87
|
-
if (attachShadow === null) {
|
|
88
|
-
attachShadow = Element.prototype.attachShadow;
|
|
89
|
-
try {
|
|
90
|
-
Element.prototype.attachShadow = function (): ShadowRoot {
|
|
91
|
-
if (core.active()) { return schedule(attachShadow.apply(this, arguments)) as ShadowRoot; }
|
|
92
|
-
else { return attachShadow.apply(this, arguments)}
|
|
93
|
-
}
|
|
94
|
-
} catch { attachShadow = null; }
|
|
95
|
-
}
|
|
111
|
+
}
|
|
96
112
|
}
|
|
97
113
|
|
|
98
114
|
export function observe(node: Node): void {
|
|
@@ -102,12 +118,14 @@ export function observe(node: Node): void {
|
|
|
102
118
|
// Also, wrap it inside a try / catch. In certain browsers (e.g. legacy Edge), observer on shadow dom can throw errors
|
|
103
119
|
try {
|
|
104
120
|
let m = api(Constant.MutationObserver);
|
|
105
|
-
let observer =
|
|
121
|
+
let observer = m in window ? new window[m](measure(handle) as MutationCallback) : null;
|
|
106
122
|
if (observer) {
|
|
107
123
|
observer.observe(node, { attributes: true, childList: true, characterData: true, subtree: true });
|
|
108
124
|
observers.push(observer);
|
|
109
125
|
}
|
|
110
|
-
} catch (e) {
|
|
126
|
+
} catch (e) {
|
|
127
|
+
internal.log(Code.MutationObserver, Severity.Info, e ? e.name : null);
|
|
128
|
+
}
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
export function monitor(frame: HTMLIFrameElement): void {
|
|
@@ -120,7 +138,11 @@ export function monitor(frame: HTMLIFrameElement): void {
|
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
export function stop(): void {
|
|
123
|
-
for (let observer of observers) {
|
|
141
|
+
for (let observer of observers) {
|
|
142
|
+
if (observer) {
|
|
143
|
+
observer.disconnect();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
124
146
|
observers = [];
|
|
125
147
|
history = {};
|
|
126
148
|
mutations = [];
|
|
@@ -141,28 +163,43 @@ function handle(m: MutationRecord[]): void {
|
|
|
141
163
|
// Queue up mutation records for asynchronous processing
|
|
142
164
|
let now = time();
|
|
143
165
|
summary.track(Event.Mutation, now);
|
|
144
|
-
mutations.push({ time: now, mutations: m});
|
|
166
|
+
mutations.push({ time: now, mutations: m });
|
|
145
167
|
task.schedule(process, Priority.High).then((): void => {
|
|
146
|
-
|
|
147
|
-
|
|
168
|
+
setTimeout(doc.compute);
|
|
169
|
+
measure(region.compute)();
|
|
148
170
|
});
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
async function processMutation(timer: Timer, mutation: MutationRecord, instance: number, timestamp: number): Promise<void> {
|
|
152
174
|
let state = task.state(timer);
|
|
153
|
-
|
|
154
|
-
if (state === Task.
|
|
175
|
+
|
|
176
|
+
if (state === Task.Wait) {
|
|
177
|
+
state = await task.suspend(timer);
|
|
178
|
+
}
|
|
179
|
+
if (state === Task.Stop) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
155
183
|
let target = mutation.target;
|
|
156
184
|
let type = config.throttleDom ? track(mutation, timer, instance, timestamp) : mutation.type;
|
|
157
|
-
|
|
158
|
-
if (type && target && target.
|
|
185
|
+
|
|
186
|
+
if (type && target && target.ownerDocument) {
|
|
187
|
+
dom.parse(target.ownerDocument);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (type && target && target.nodeType == Node.DOCUMENT_FRAGMENT_NODE && (target as ShadowRoot).host) {
|
|
191
|
+
dom.parse(target as ShadowRoot);
|
|
192
|
+
}
|
|
193
|
+
|
|
159
194
|
switch (type) {
|
|
160
195
|
case Constant.Attributes:
|
|
196
|
+
if (IGNORED_ATTRIBUTES.indexOf(mutation.attributeName) < 0) {
|
|
161
197
|
processNode(target, Source.Attributes, timestamp);
|
|
162
|
-
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
163
200
|
case Constant.CharacterData:
|
|
164
|
-
|
|
165
|
-
|
|
201
|
+
processNode(target, Source.CharacterData, timestamp);
|
|
202
|
+
break;
|
|
166
203
|
case Constant.ChildList:
|
|
167
204
|
processNodeList(mutation.addedNodes, Source.ChildListAdd, timer, timestamp);
|
|
168
205
|
processNodeList(mutation.removedNodes, Source.ChildListRemove, timer, timestamp);
|
|
@@ -179,7 +216,7 @@ async function process(): Promise<void> {
|
|
|
179
216
|
let record = mutations.shift();
|
|
180
217
|
let instance = time();
|
|
181
218
|
for (let mutation of record.mutations) {
|
|
182
|
-
await processMutation(timer, mutation, instance, record.time)
|
|
219
|
+
await processMutation(timer, mutation, instance, record.time);
|
|
183
220
|
}
|
|
184
221
|
await encode(Event.Mutation, timer, record.time);
|
|
185
222
|
}
|
|
@@ -202,13 +239,13 @@ async function process(): Promise<void> {
|
|
|
202
239
|
}
|
|
203
240
|
|
|
204
241
|
cleanHistory();
|
|
205
|
-
|
|
242
|
+
|
|
206
243
|
task.stop(timer);
|
|
207
244
|
}
|
|
208
245
|
|
|
209
246
|
function cleanHistory(): void {
|
|
210
247
|
let now = time();
|
|
211
|
-
if (Object.keys(history).length > Setting.MaxMutationHistoryCount) {
|
|
248
|
+
if (Object.keys(history).length > Setting.MaxMutationHistoryCount) {
|
|
212
249
|
history = {};
|
|
213
250
|
metric.count(Metric.HistoryClear);
|
|
214
251
|
}
|
|
@@ -223,33 +260,40 @@ function cleanHistory(): void {
|
|
|
223
260
|
|
|
224
261
|
function track(m: MutationRecord, timer: Timer, instance: number, timestamp: number): string {
|
|
225
262
|
let value = m.target ? dom.get(m.target.parentNode) : null;
|
|
263
|
+
|
|
226
264
|
// Check if the parent is already discovered and that the parent is not the document root
|
|
227
265
|
if (value && value.data.tag !== Constant.HTML) {
|
|
228
266
|
// calculate inactive period based on the timestamp of the mutation not when the mutation is processed
|
|
229
267
|
let inactive = timestamp > activePeriod;
|
|
268
|
+
|
|
230
269
|
// Calculate critical period based on when mutation is processed
|
|
231
270
|
const critical = instance < criticalPeriod;
|
|
232
271
|
let target = dom.get(m.target);
|
|
233
272
|
let element = target && target.selector ? target.selector.join() : m.target.nodeName;
|
|
234
273
|
let parent = value.selector ? value.selector.join() : Constant.Empty;
|
|
274
|
+
|
|
235
275
|
// Check if its a low priority (e.g., ads related) element mutation happening during critical period
|
|
236
276
|
// If the discard list is empty, we discard all mutations during critical period
|
|
237
|
-
const lowPriMutation =
|
|
238
|
-
|
|
239
|
-
(config.discard.length === 0 || config.discard.some((key) => element.includes(key)));
|
|
277
|
+
const lowPriMutation = config.throttleMutations && critical && (config.discard.length === 0 || config.discard.some((key) => element.includes(key)));
|
|
278
|
+
|
|
240
279
|
// We use selector, instead of id, to determine the key (signature for the mutation) because in some cases
|
|
241
280
|
// repeated mutations can cause elements to be destroyed and then recreated as new DOM nodes
|
|
242
281
|
// In those cases, IDs will change however the selector (which is relative to DOM xPath) remains the same
|
|
243
282
|
let key = [parent, element, m.attributeName, names(m.addedNodes), names(m.removedNodes)].join();
|
|
283
|
+
|
|
244
284
|
// Initialize an entry if it doesn't already exist
|
|
245
285
|
history[key] = key in history ? history[key] : [0, instance];
|
|
246
286
|
let h = history[key];
|
|
287
|
+
|
|
247
288
|
// Lookup any pending nodes queued up for removal, and process them now if we suspended a mutation before
|
|
248
|
-
if (inactive === false && h[0] >= Setting.MutationSuspendThreshold) {
|
|
249
|
-
|
|
289
|
+
if (inactive === false && h[0] >= Setting.MutationSuspendThreshold) {
|
|
290
|
+
processNodeList(h[2], Source.ChildListRemove, timer, timestamp);
|
|
291
|
+
}
|
|
292
|
+
|
|
250
293
|
// Update the counter, do not reset counter if its critical period
|
|
251
294
|
h[0] = inactive || lowPriMutation ? (h[1] === instance ? h[0] : h[0] + 1) : 1;
|
|
252
295
|
h[1] = instance;
|
|
296
|
+
|
|
253
297
|
// Return updated mutation type based on,
|
|
254
298
|
// 1. if we have already hit the threshold or not
|
|
255
299
|
// 2. if its a low priority mutation happening during critical time period
|
|
@@ -260,11 +304,13 @@ function track(m: MutationRecord, timer: Timer, instance: number, timestamp: num
|
|
|
260
304
|
if (instance > timestamp + Setting.MutationActivePeriod) {
|
|
261
305
|
return m.type;
|
|
262
306
|
}
|
|
307
|
+
|
|
263
308
|
if (!config.dropMutations) {
|
|
264
309
|
// we only store the most recent mutation for a given key if it is being throttled
|
|
265
|
-
throttledMutations[key] = {mutation: m, timestamp};
|
|
310
|
+
throttledMutations[key] = { mutation: m, timestamp };
|
|
266
311
|
}
|
|
267
|
-
|
|
312
|
+
|
|
313
|
+
return Constant.Throttle;
|
|
268
314
|
}
|
|
269
315
|
}
|
|
270
316
|
return m.type;
|
|
@@ -272,7 +318,9 @@ function track(m: MutationRecord, timer: Timer, instance: number, timestamp: num
|
|
|
272
318
|
|
|
273
319
|
function names(nodes: NodeList): string {
|
|
274
320
|
let output: string[] = [];
|
|
275
|
-
for (let i = 0; nodes && i < nodes.length; i++) {
|
|
321
|
+
for (let i = 0; nodes && i < nodes.length; i++) {
|
|
322
|
+
output.push(nodes[i].nodeName);
|
|
323
|
+
}
|
|
276
324
|
return output.join();
|
|
277
325
|
}
|
|
278
326
|
|
|
@@ -284,8 +332,12 @@ async function processNodeList(list: NodeList, source: Source, timer: Timer, tim
|
|
|
284
332
|
traverse(node, timer, source, timestamp);
|
|
285
333
|
} else {
|
|
286
334
|
let state = task.state(timer);
|
|
287
|
-
if (state === Task.Wait) {
|
|
288
|
-
|
|
335
|
+
if (state === Task.Wait) {
|
|
336
|
+
state = await task.suspend(timer);
|
|
337
|
+
}
|
|
338
|
+
if (state === Task.Stop) {
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
289
341
|
processNode(node, source, timestamp);
|
|
290
342
|
}
|
|
291
343
|
}
|
|
@@ -295,18 +347,26 @@ function processThrottledMutations(): void {
|
|
|
295
347
|
if (throttleDelay) {
|
|
296
348
|
clearTimeout(throttleDelay);
|
|
297
349
|
}
|
|
298
|
-
throttleDelay = setTimeout(() => {
|
|
350
|
+
throttleDelay = setTimeout(() => {
|
|
351
|
+
task.schedule(process, Priority.High);
|
|
352
|
+
}, Setting.LookAhead);
|
|
299
353
|
}
|
|
300
354
|
|
|
301
355
|
export function schedule(node: Node): Node {
|
|
302
356
|
// Only schedule manual trigger for this node if it's not already in the queue
|
|
303
|
-
if (queue.indexOf(node) < 0) {
|
|
357
|
+
if (queue.indexOf(node) < 0) {
|
|
358
|
+
queue.push(node);
|
|
359
|
+
}
|
|
304
360
|
|
|
305
361
|
// Cancel any previous trigger before scheduling a new one.
|
|
306
362
|
// It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
|
|
307
363
|
// And in those cases we do not wish to monitor changes multiple times for the same node.
|
|
308
|
-
if (timeout) {
|
|
309
|
-
|
|
364
|
+
if (timeout) {
|
|
365
|
+
clearTimeout(timeout);
|
|
366
|
+
}
|
|
367
|
+
timeout = setTimeout(() => {
|
|
368
|
+
trigger();
|
|
369
|
+
}, Setting.LookAhead);
|
|
310
370
|
|
|
311
371
|
return node;
|
|
312
372
|
}
|
|
@@ -317,7 +377,9 @@ function trigger(): void {
|
|
|
317
377
|
if (node) {
|
|
318
378
|
let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
|
319
379
|
// Skip re-processing shadowRoot if it was already discovered
|
|
320
|
-
if (shadowRoot && dom.has(node)) {
|
|
380
|
+
if (shadowRoot && dom.has(node)) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
321
383
|
generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
|
|
322
384
|
}
|
|
323
385
|
}
|
|
@@ -326,15 +388,17 @@ function trigger(): void {
|
|
|
326
388
|
|
|
327
389
|
function generate(target: Node, type: MutationRecordType): void {
|
|
328
390
|
generate.dn = FunctionNames.MutationGenerate;
|
|
329
|
-
measure(handle)([
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
391
|
+
measure(handle)([
|
|
392
|
+
{
|
|
393
|
+
addedNodes: [target],
|
|
394
|
+
attributeName: null,
|
|
395
|
+
attributeNamespace: null,
|
|
396
|
+
nextSibling: null,
|
|
397
|
+
oldValue: null,
|
|
398
|
+
previousSibling: null,
|
|
399
|
+
removedNodes: [],
|
|
400
|
+
target,
|
|
401
|
+
type,
|
|
402
|
+
},
|
|
403
|
+
]);
|
|
340
404
|
}
|
package/src/layout/style.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Event, Metric } from "@clarity-types/data";
|
|
2
2
|
import { StyleSheetOperation, StyleSheetState } from "@clarity-types/layout";
|
|
3
3
|
import { time } from "@src/core/time";
|
|
4
|
-
import { shortid
|
|
4
|
+
import { shortid } from "@src/data/metadata";
|
|
5
5
|
import encode from "@src/layout/encode";
|
|
6
6
|
import { getId } from "@src/layout/dom";
|
|
7
7
|
import * as core from "@src/core";
|
|
@@ -13,10 +13,10 @@ export let sheetAdoptionState: StyleSheetState[] = [];
|
|
|
13
13
|
let replace: (text?: string) => Promise<CSSStyleSheet> = null;
|
|
14
14
|
let replaceSync: (text?: string) => void = null;
|
|
15
15
|
const styleSheetId = 'claritySheetId';
|
|
16
|
-
const styleSheetPageNum = 'claritySheetNum';
|
|
17
16
|
let styleSheetMap = {};
|
|
18
17
|
let styleTimeMap: {[key: string]: number} = {};
|
|
19
18
|
let documentNodes = [];
|
|
19
|
+
let createdSheetIds = [];
|
|
20
20
|
|
|
21
21
|
export function start(): void {
|
|
22
22
|
if (window['CSSStyleSheet'] && CSSStyleSheet.prototype) {
|
|
@@ -28,7 +28,7 @@ export function start(): void {
|
|
|
28
28
|
// if we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it
|
|
29
29
|
// and attached the sheet to a document. This way the timestamp of the style sheet creation will align
|
|
30
30
|
// to when it is used in the document rather than potentially being misaligned during the traverse process.
|
|
31
|
-
if (this[
|
|
31
|
+
if (createdSheetIds.indexOf(this[styleSheetId]) > -1) {
|
|
32
32
|
trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Replace, arguments[0]);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
@@ -44,7 +44,7 @@ export function start(): void {
|
|
|
44
44
|
// if we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it
|
|
45
45
|
// and attached the sheet to a document. This way the timestamp of the style sheet creation will align
|
|
46
46
|
// to when it is used in the document rather than potentially being misaligned during the traverse process.
|
|
47
|
-
if (this[
|
|
47
|
+
if (createdSheetIds.indexOf(this[styleSheetId]) > -1) {
|
|
48
48
|
trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.ReplaceSync, arguments[0]);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -66,17 +66,17 @@ export function checkDocumentStyles(documentNode: Document, timestamp: number):
|
|
|
66
66
|
metric.max(Metric.ConstructedStyles, 1);
|
|
67
67
|
let currentStyleSheets: string[] = [];
|
|
68
68
|
for (var styleSheet of documentNode.adoptedStyleSheets) {
|
|
69
|
-
const pageNum = metadataFields.pageNum;
|
|
70
69
|
// If we haven't seen this style sheet on this page yet, we create a reference to it for the visualizer.
|
|
71
70
|
// For SPA or times in which Clarity restarts on a given page, our visualizer would lose context
|
|
72
71
|
// on the previously created style sheet for page N-1.
|
|
73
72
|
// Then we synthetically call replaceSync with its contents to bootstrap it
|
|
74
|
-
if (styleSheet[
|
|
75
|
-
styleSheet[styleSheetPageNum] = pageNum;
|
|
73
|
+
if (!styleSheet[styleSheetId] || createdSheetIds.indexOf(styleSheet[styleSheetId]) === -1) {
|
|
76
74
|
styleSheet[styleSheetId] = shortid();
|
|
75
|
+
createdSheetIds.push(styleSheet[styleSheetId]);
|
|
77
76
|
trackStyleChange(timestamp, styleSheet[styleSheetId], StyleSheetOperation.Create);
|
|
78
77
|
trackStyleChange(timestamp, styleSheet[styleSheetId], StyleSheetOperation.ReplaceSync, getCssRules(styleSheet));
|
|
79
78
|
}
|
|
79
|
+
|
|
80
80
|
currentStyleSheets.push(styleSheet[styleSheetId]);
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -109,6 +109,7 @@ export function stop(): void {
|
|
|
109
109
|
styleSheetMap = {};
|
|
110
110
|
styleTimeMap = {};
|
|
111
111
|
documentNodes = [];
|
|
112
|
+
createdSheetIds = [];
|
|
112
113
|
reset();
|
|
113
114
|
}
|
|
114
115
|
|
package/types/core.d.ts
CHANGED
package/types/data.d.ts
CHANGED
|
@@ -433,6 +433,12 @@ export interface BaselineData {
|
|
|
433
433
|
downX: number;
|
|
434
434
|
downY: number;
|
|
435
435
|
downTime: number;
|
|
436
|
+
upX: number;
|
|
437
|
+
upY: number;
|
|
438
|
+
upTime: number;
|
|
439
|
+
pointerPrevX: number;
|
|
440
|
+
pointerPrevY: number;
|
|
441
|
+
pointerPrevTime: number;
|
|
436
442
|
}
|
|
437
443
|
|
|
438
444
|
export interface IdentityData {
|
package/types/interaction.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BooleanFlag } from "@clarity-types/data";
|
|
1
2
|
import { Time } from "./core";
|
|
2
3
|
import { Event, Target } from "./data";
|
|
3
4
|
|
|
@@ -121,6 +122,12 @@ export interface ClickData {
|
|
|
121
122
|
link: string;
|
|
122
123
|
hash: string;
|
|
123
124
|
trust: number;
|
|
125
|
+
isFullText: BooleanFlag;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface TextInfo {
|
|
129
|
+
text: string;
|
|
130
|
+
isFullText: BooleanFlag;
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
export interface ClipboardData {
|
|
@@ -150,6 +157,7 @@ export interface SelectionData {
|
|
|
150
157
|
|
|
151
158
|
export interface UnloadData {
|
|
152
159
|
name: string;
|
|
160
|
+
persisted: BooleanFlag;
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
export interface VisibilityData {
|