clarity-js 0.7.68 → 0.7.70
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 +66 -11
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +66 -11
- package/build/clarity.performance.js +1 -1
- package/package.json +1 -1
- 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/timeline.ts +1 -1
- package/src/layout/mutation.ts +166 -94
- package/types/data.d.ts +6 -0
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[] = [];
|
|
@@ -35,64 +35,82 @@ let throttleDelay: number = null;
|
|
|
35
35
|
let activePeriod = null;
|
|
36
36
|
let history: MutationHistory = {};
|
|
37
37
|
let criticalPeriod = null;
|
|
38
|
+
let observedNodes: WeakMap<Node, MutationObserver> = new WeakMap<Node, MutationObserver>();
|
|
39
|
+
|
|
40
|
+
// We ignore mutations if these attributes are updated
|
|
41
|
+
const IGNORED_ATTRIBUTES = ["data-google-query-id", "data-load-complete", "data-google-container-id"];
|
|
38
42
|
|
|
39
43
|
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
|
-
}
|
|
44
|
+
start.dn = FunctionNames.MutationStart;
|
|
45
|
+
observers = [];
|
|
46
|
+
queue = [];
|
|
47
|
+
timeout = null;
|
|
48
|
+
activePeriod = 0;
|
|
49
|
+
history = {};
|
|
50
|
+
criticalPeriod = 0;
|
|
51
|
+
observedNodes = new WeakMap<Node, MutationObserver>();
|
|
59
52
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
53
|
+
// Some popular open source libraries, like styled-components, optimize performance
|
|
54
|
+
// by injecting CSS using insertRule API vs. appending text node. A side effect of
|
|
55
|
+
// using javascript API is that it doesn't trigger DOM mutation and therefore we
|
|
56
|
+
// need to override the insertRule API and listen for changes manually.
|
|
57
|
+
if (insertRule === null) {
|
|
58
|
+
insertRule = CSSStyleSheet.prototype.insertRule;
|
|
59
|
+
CSSStyleSheet.prototype.insertRule = function (): number {
|
|
60
|
+
if (core.active()) {
|
|
61
|
+
schedule(this.ownerNode);
|
|
62
|
+
}
|
|
63
|
+
return insertRule.apply(this, arguments);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
67
|
+
if ("CSSMediaRule" in window && mediaInsertRule === null) {
|
|
68
|
+
mediaInsertRule = CSSMediaRule.prototype.insertRule;
|
|
69
|
+
CSSMediaRule.prototype.insertRule = function (): number {
|
|
70
|
+
if (core.active()) {
|
|
71
|
+
schedule(this.parentStyleSheet.ownerNode);
|
|
72
|
+
}
|
|
73
|
+
return mediaInsertRule.apply(this, arguments);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (deleteRule === null) {
|
|
78
|
+
deleteRule = CSSStyleSheet.prototype.deleteRule;
|
|
79
|
+
CSSStyleSheet.prototype.deleteRule = function (): void {
|
|
80
|
+
if (core.active()) {
|
|
81
|
+
schedule(this.ownerNode);
|
|
82
|
+
}
|
|
83
|
+
return deleteRule.apply(this, arguments);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
75
86
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
if ("CSSMediaRule" in window && mediaDeleteRule === null) {
|
|
88
|
+
mediaDeleteRule = CSSMediaRule.prototype.deleteRule;
|
|
89
|
+
CSSMediaRule.prototype.deleteRule = function (): void {
|
|
90
|
+
if (core.active()) {
|
|
91
|
+
schedule(this.parentStyleSheet.ownerNode);
|
|
92
|
+
}
|
|
93
|
+
return mediaDeleteRule.apply(this, arguments);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Add a hook to attachShadow API calls
|
|
98
|
+
// In case we are unable to add a hook and browser throws an exception,
|
|
99
|
+
// reset attachShadow variable and resume processing like before
|
|
100
|
+
if (attachShadow === null) {
|
|
101
|
+
attachShadow = Element.prototype.attachShadow;
|
|
102
|
+
try {
|
|
103
|
+
Element.prototype.attachShadow = function (): ShadowRoot {
|
|
104
|
+
if (core.active()) {
|
|
105
|
+
return schedule(attachShadow.apply(this, arguments)) as ShadowRoot;
|
|
106
|
+
} else {
|
|
107
|
+
return attachShadow.apply(this, arguments);
|
|
108
|
+
}
|
|
81
109
|
};
|
|
110
|
+
} catch {
|
|
111
|
+
attachShadow = null;
|
|
82
112
|
}
|
|
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
|
-
}
|
|
113
|
+
}
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
export function observe(node: Node): void {
|
|
@@ -101,13 +119,21 @@ export function observe(node: Node): void {
|
|
|
101
119
|
// For this reason, we need to wire up mutations every time we see a new shadow dom.
|
|
102
120
|
// Also, wrap it inside a try / catch. In certain browsers (e.g. legacy Edge), observer on shadow dom can throw errors
|
|
103
121
|
try {
|
|
122
|
+
// Cleanup old observer if present.
|
|
123
|
+
if (observedNodes.has(node)) {
|
|
124
|
+
observedNodes.get(node)?.disconnect();
|
|
125
|
+
}
|
|
126
|
+
|
|
104
127
|
let m = api(Constant.MutationObserver);
|
|
105
|
-
let observer =
|
|
128
|
+
let observer = m in window ? new window[m](measure(handle) as MutationCallback) : null;
|
|
106
129
|
if (observer) {
|
|
107
130
|
observer.observe(node, { attributes: true, childList: true, characterData: true, subtree: true });
|
|
131
|
+
observedNodes.set(node, observer);
|
|
108
132
|
observers.push(observer);
|
|
109
133
|
}
|
|
110
|
-
} catch (e) {
|
|
134
|
+
} catch (e) {
|
|
135
|
+
internal.log(Code.MutationObserver, Severity.Info, e ? e.name : null);
|
|
136
|
+
}
|
|
111
137
|
}
|
|
112
138
|
|
|
113
139
|
export function monitor(frame: HTMLIFrameElement): void {
|
|
@@ -120,7 +146,11 @@ export function monitor(frame: HTMLIFrameElement): void {
|
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
export function stop(): void {
|
|
123
|
-
for (let observer of observers) {
|
|
149
|
+
for (let observer of observers) {
|
|
150
|
+
if (observer) {
|
|
151
|
+
observer.disconnect();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
124
154
|
observers = [];
|
|
125
155
|
history = {};
|
|
126
156
|
mutations = [];
|
|
@@ -141,28 +171,43 @@ function handle(m: MutationRecord[]): void {
|
|
|
141
171
|
// Queue up mutation records for asynchronous processing
|
|
142
172
|
let now = time();
|
|
143
173
|
summary.track(Event.Mutation, now);
|
|
144
|
-
mutations.push({ time: now, mutations: m});
|
|
174
|
+
mutations.push({ time: now, mutations: m });
|
|
145
175
|
task.schedule(process, Priority.High).then((): void => {
|
|
146
|
-
|
|
147
|
-
|
|
176
|
+
setTimeout(doc.compute);
|
|
177
|
+
measure(region.compute)();
|
|
148
178
|
});
|
|
149
179
|
}
|
|
150
180
|
|
|
151
181
|
async function processMutation(timer: Timer, mutation: MutationRecord, instance: number, timestamp: number): Promise<void> {
|
|
152
182
|
let state = task.state(timer);
|
|
153
|
-
|
|
154
|
-
if (state === Task.
|
|
183
|
+
|
|
184
|
+
if (state === Task.Wait) {
|
|
185
|
+
state = await task.suspend(timer);
|
|
186
|
+
}
|
|
187
|
+
if (state === Task.Stop) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
155
191
|
let target = mutation.target;
|
|
156
192
|
let type = config.throttleDom ? track(mutation, timer, instance, timestamp) : mutation.type;
|
|
157
|
-
|
|
158
|
-
if (type && target && target.
|
|
193
|
+
|
|
194
|
+
if (type && target && target.ownerDocument) {
|
|
195
|
+
dom.parse(target.ownerDocument);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (type && target && target.nodeType == Node.DOCUMENT_FRAGMENT_NODE && (target as ShadowRoot).host) {
|
|
199
|
+
dom.parse(target as ShadowRoot);
|
|
200
|
+
}
|
|
201
|
+
|
|
159
202
|
switch (type) {
|
|
160
203
|
case Constant.Attributes:
|
|
204
|
+
if (IGNORED_ATTRIBUTES.indexOf(mutation.attributeName) < 0) {
|
|
161
205
|
processNode(target, Source.Attributes, timestamp);
|
|
162
|
-
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
163
208
|
case Constant.CharacterData:
|
|
164
|
-
|
|
165
|
-
|
|
209
|
+
processNode(target, Source.CharacterData, timestamp);
|
|
210
|
+
break;
|
|
166
211
|
case Constant.ChildList:
|
|
167
212
|
processNodeList(mutation.addedNodes, Source.ChildListAdd, timer, timestamp);
|
|
168
213
|
processNodeList(mutation.removedNodes, Source.ChildListRemove, timer, timestamp);
|
|
@@ -179,7 +224,7 @@ async function process(): Promise<void> {
|
|
|
179
224
|
let record = mutations.shift();
|
|
180
225
|
let instance = time();
|
|
181
226
|
for (let mutation of record.mutations) {
|
|
182
|
-
await processMutation(timer, mutation, instance, record.time)
|
|
227
|
+
await processMutation(timer, mutation, instance, record.time);
|
|
183
228
|
}
|
|
184
229
|
await encode(Event.Mutation, timer, record.time);
|
|
185
230
|
}
|
|
@@ -202,13 +247,13 @@ async function process(): Promise<void> {
|
|
|
202
247
|
}
|
|
203
248
|
|
|
204
249
|
cleanHistory();
|
|
205
|
-
|
|
250
|
+
|
|
206
251
|
task.stop(timer);
|
|
207
252
|
}
|
|
208
253
|
|
|
209
254
|
function cleanHistory(): void {
|
|
210
255
|
let now = time();
|
|
211
|
-
if (Object.keys(history).length > Setting.MaxMutationHistoryCount) {
|
|
256
|
+
if (Object.keys(history).length > Setting.MaxMutationHistoryCount) {
|
|
212
257
|
history = {};
|
|
213
258
|
metric.count(Metric.HistoryClear);
|
|
214
259
|
}
|
|
@@ -223,33 +268,40 @@ function cleanHistory(): void {
|
|
|
223
268
|
|
|
224
269
|
function track(m: MutationRecord, timer: Timer, instance: number, timestamp: number): string {
|
|
225
270
|
let value = m.target ? dom.get(m.target.parentNode) : null;
|
|
271
|
+
|
|
226
272
|
// Check if the parent is already discovered and that the parent is not the document root
|
|
227
273
|
if (value && value.data.tag !== Constant.HTML) {
|
|
228
274
|
// calculate inactive period based on the timestamp of the mutation not when the mutation is processed
|
|
229
275
|
let inactive = timestamp > activePeriod;
|
|
276
|
+
|
|
230
277
|
// Calculate critical period based on when mutation is processed
|
|
231
278
|
const critical = instance < criticalPeriod;
|
|
232
279
|
let target = dom.get(m.target);
|
|
233
280
|
let element = target && target.selector ? target.selector.join() : m.target.nodeName;
|
|
234
281
|
let parent = value.selector ? value.selector.join() : Constant.Empty;
|
|
282
|
+
|
|
235
283
|
// Check if its a low priority (e.g., ads related) element mutation happening during critical period
|
|
236
284
|
// 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)));
|
|
285
|
+
const lowPriMutation = config.throttleMutations && critical && (config.discard.length === 0 || config.discard.some((key) => element.includes(key)));
|
|
286
|
+
|
|
240
287
|
// We use selector, instead of id, to determine the key (signature for the mutation) because in some cases
|
|
241
288
|
// repeated mutations can cause elements to be destroyed and then recreated as new DOM nodes
|
|
242
289
|
// In those cases, IDs will change however the selector (which is relative to DOM xPath) remains the same
|
|
243
290
|
let key = [parent, element, m.attributeName, names(m.addedNodes), names(m.removedNodes)].join();
|
|
291
|
+
|
|
244
292
|
// Initialize an entry if it doesn't already exist
|
|
245
293
|
history[key] = key in history ? history[key] : [0, instance];
|
|
246
294
|
let h = history[key];
|
|
295
|
+
|
|
247
296
|
// 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
|
-
|
|
297
|
+
if (inactive === false && h[0] >= Setting.MutationSuspendThreshold) {
|
|
298
|
+
processNodeList(h[2], Source.ChildListRemove, timer, timestamp);
|
|
299
|
+
}
|
|
300
|
+
|
|
250
301
|
// Update the counter, do not reset counter if its critical period
|
|
251
302
|
h[0] = inactive || lowPriMutation ? (h[1] === instance ? h[0] : h[0] + 1) : 1;
|
|
252
303
|
h[1] = instance;
|
|
304
|
+
|
|
253
305
|
// Return updated mutation type based on,
|
|
254
306
|
// 1. if we have already hit the threshold or not
|
|
255
307
|
// 2. if its a low priority mutation happening during critical time period
|
|
@@ -260,11 +312,13 @@ function track(m: MutationRecord, timer: Timer, instance: number, timestamp: num
|
|
|
260
312
|
if (instance > timestamp + Setting.MutationActivePeriod) {
|
|
261
313
|
return m.type;
|
|
262
314
|
}
|
|
315
|
+
|
|
263
316
|
if (!config.dropMutations) {
|
|
264
317
|
// we only store the most recent mutation for a given key if it is being throttled
|
|
265
|
-
throttledMutations[key] = {mutation: m, timestamp};
|
|
318
|
+
throttledMutations[key] = { mutation: m, timestamp };
|
|
266
319
|
}
|
|
267
|
-
|
|
320
|
+
|
|
321
|
+
return Constant.Throttle;
|
|
268
322
|
}
|
|
269
323
|
}
|
|
270
324
|
return m.type;
|
|
@@ -272,7 +326,9 @@ function track(m: MutationRecord, timer: Timer, instance: number, timestamp: num
|
|
|
272
326
|
|
|
273
327
|
function names(nodes: NodeList): string {
|
|
274
328
|
let output: string[] = [];
|
|
275
|
-
for (let i = 0; nodes && i < nodes.length; i++) {
|
|
329
|
+
for (let i = 0; nodes && i < nodes.length; i++) {
|
|
330
|
+
output.push(nodes[i].nodeName);
|
|
331
|
+
}
|
|
276
332
|
return output.join();
|
|
277
333
|
}
|
|
278
334
|
|
|
@@ -284,8 +340,12 @@ async function processNodeList(list: NodeList, source: Source, timer: Timer, tim
|
|
|
284
340
|
traverse(node, timer, source, timestamp);
|
|
285
341
|
} else {
|
|
286
342
|
let state = task.state(timer);
|
|
287
|
-
if (state === Task.Wait) {
|
|
288
|
-
|
|
343
|
+
if (state === Task.Wait) {
|
|
344
|
+
state = await task.suspend(timer);
|
|
345
|
+
}
|
|
346
|
+
if (state === Task.Stop) {
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
289
349
|
processNode(node, source, timestamp);
|
|
290
350
|
}
|
|
291
351
|
}
|
|
@@ -295,18 +355,26 @@ function processThrottledMutations(): void {
|
|
|
295
355
|
if (throttleDelay) {
|
|
296
356
|
clearTimeout(throttleDelay);
|
|
297
357
|
}
|
|
298
|
-
throttleDelay = setTimeout(() => {
|
|
358
|
+
throttleDelay = setTimeout(() => {
|
|
359
|
+
task.schedule(process, Priority.High);
|
|
360
|
+
}, Setting.LookAhead);
|
|
299
361
|
}
|
|
300
362
|
|
|
301
363
|
export function schedule(node: Node): Node {
|
|
302
364
|
// Only schedule manual trigger for this node if it's not already in the queue
|
|
303
|
-
if (queue.indexOf(node) < 0) {
|
|
365
|
+
if (queue.indexOf(node) < 0) {
|
|
366
|
+
queue.push(node);
|
|
367
|
+
}
|
|
304
368
|
|
|
305
369
|
// Cancel any previous trigger before scheduling a new one.
|
|
306
370
|
// It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
|
|
307
371
|
// And in those cases we do not wish to monitor changes multiple times for the same node.
|
|
308
|
-
if (timeout) {
|
|
309
|
-
|
|
372
|
+
if (timeout) {
|
|
373
|
+
clearTimeout(timeout);
|
|
374
|
+
}
|
|
375
|
+
timeout = setTimeout(() => {
|
|
376
|
+
trigger();
|
|
377
|
+
}, Setting.LookAhead);
|
|
310
378
|
|
|
311
379
|
return node;
|
|
312
380
|
}
|
|
@@ -317,7 +385,9 @@ function trigger(): void {
|
|
|
317
385
|
if (node) {
|
|
318
386
|
let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
|
319
387
|
// Skip re-processing shadowRoot if it was already discovered
|
|
320
|
-
if (shadowRoot && dom.has(node)) {
|
|
388
|
+
if (shadowRoot && dom.has(node)) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
321
391
|
generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
|
|
322
392
|
}
|
|
323
393
|
}
|
|
@@ -326,15 +396,17 @@ function trigger(): void {
|
|
|
326
396
|
|
|
327
397
|
function generate(target: Node, type: MutationRecordType): void {
|
|
328
398
|
generate.dn = FunctionNames.MutationGenerate;
|
|
329
|
-
measure(handle)([
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
399
|
+
measure(handle)([
|
|
400
|
+
{
|
|
401
|
+
addedNodes: [target],
|
|
402
|
+
attributeName: null,
|
|
403
|
+
attributeNamespace: null,
|
|
404
|
+
nextSibling: null,
|
|
405
|
+
oldValue: null,
|
|
406
|
+
previousSibling: null,
|
|
407
|
+
removedNodes: [],
|
|
408
|
+
target,
|
|
409
|
+
type,
|
|
410
|
+
},
|
|
411
|
+
]);
|
|
340
412
|
}
|
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 {
|