clarity-js 0.8.42 → 0.8.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +26 -26
  2. package/build/clarity.extended.js +1 -1
  3. package/build/clarity.insight.js +1 -1
  4. package/build/clarity.js +6043 -6030
  5. package/build/clarity.min.js +1 -1
  6. package/build/clarity.module.js +6043 -6030
  7. package/build/clarity.performance.js +1 -1
  8. package/package.json +70 -70
  9. package/rollup.config.ts +161 -161
  10. package/src/clarity.ts +65 -65
  11. package/src/core/api.ts +8 -8
  12. package/src/core/config.ts +29 -29
  13. package/src/core/copy.ts +3 -3
  14. package/src/core/dynamic.ts +13 -7
  15. package/src/core/event.ts +53 -53
  16. package/src/core/hash.ts +19 -19
  17. package/src/core/history.ts +71 -71
  18. package/src/core/index.ts +81 -81
  19. package/src/core/measure.ts +19 -19
  20. package/src/core/report.ts +28 -28
  21. package/src/core/scrub.ts +204 -202
  22. package/src/core/task.ts +181 -181
  23. package/src/core/throttle.ts +46 -46
  24. package/src/core/time.ts +26 -26
  25. package/src/core/timeout.ts +10 -10
  26. package/src/core/version.ts +2 -2
  27. package/src/data/baseline.ts +162 -162
  28. package/src/data/compress.ts +31 -31
  29. package/src/data/consent.ts +77 -77
  30. package/src/data/custom.ts +23 -23
  31. package/src/data/dimension.ts +53 -53
  32. package/src/data/encode.ts +155 -155
  33. package/src/data/envelope.ts +53 -53
  34. package/src/data/extract.ts +211 -211
  35. package/src/data/index.ts +50 -50
  36. package/src/data/limit.ts +44 -44
  37. package/src/data/metadata.ts +411 -408
  38. package/src/data/metric.ts +51 -51
  39. package/src/data/ping.ts +36 -36
  40. package/src/data/signal.ts +30 -30
  41. package/src/data/summary.ts +34 -34
  42. package/src/data/token.ts +39 -39
  43. package/src/data/upgrade.ts +44 -44
  44. package/src/data/upload.ts +333 -333
  45. package/src/data/variable.ts +83 -83
  46. package/src/diagnostic/encode.ts +40 -40
  47. package/src/diagnostic/fraud.ts +36 -36
  48. package/src/diagnostic/index.ts +13 -13
  49. package/src/diagnostic/internal.ts +28 -28
  50. package/src/diagnostic/script.ts +35 -35
  51. package/src/dynamic/agent/blank.ts +2 -2
  52. package/src/dynamic/agent/crisp.ts +40 -40
  53. package/src/dynamic/agent/encode.ts +25 -25
  54. package/src/dynamic/agent/index.ts +8 -8
  55. package/src/dynamic/agent/livechat.ts +58 -58
  56. package/src/dynamic/agent/tidio.ts +44 -44
  57. package/src/global.ts +6 -6
  58. package/src/index.ts +9 -9
  59. package/src/insight/blank.ts +14 -14
  60. package/src/insight/encode.ts +60 -60
  61. package/src/insight/snapshot.ts +114 -114
  62. package/src/interaction/change.ts +38 -38
  63. package/src/interaction/click.ts +173 -173
  64. package/src/interaction/clipboard.ts +32 -32
  65. package/src/interaction/encode.ts +210 -210
  66. package/src/interaction/index.ts +60 -60
  67. package/src/interaction/input.ts +57 -57
  68. package/src/interaction/pointer.ts +137 -137
  69. package/src/interaction/resize.ts +50 -50
  70. package/src/interaction/scroll.ts +129 -129
  71. package/src/interaction/selection.ts +66 -66
  72. package/src/interaction/submit.ts +30 -30
  73. package/src/interaction/timeline.ts +69 -69
  74. package/src/interaction/unload.ts +26 -26
  75. package/src/interaction/visibility.ts +27 -27
  76. package/src/layout/animation.ts +133 -133
  77. package/src/layout/custom.ts +42 -42
  78. package/src/layout/discover.ts +31 -31
  79. package/src/layout/document.ts +46 -46
  80. package/src/layout/dom.ts +439 -439
  81. package/src/layout/encode.ts +154 -154
  82. package/src/layout/index.ts +42 -42
  83. package/src/layout/mutation.ts +411 -411
  84. package/src/layout/node.ts +294 -294
  85. package/src/layout/offset.ts +19 -19
  86. package/src/layout/region.ts +151 -151
  87. package/src/layout/schema.ts +63 -63
  88. package/src/layout/selector.ts +82 -82
  89. package/src/layout/style.ts +159 -159
  90. package/src/layout/target.ts +32 -32
  91. package/src/layout/traverse.ts +27 -27
  92. package/src/performance/blank.ts +9 -9
  93. package/src/performance/encode.ts +31 -31
  94. package/src/performance/index.ts +12 -12
  95. package/src/performance/interaction.ts +125 -125
  96. package/src/performance/navigation.ts +31 -31
  97. package/src/performance/observer.ts +112 -112
  98. package/src/queue.ts +33 -33
  99. package/test/core.test.ts +139 -139
  100. package/test/helper.ts +162 -162
  101. package/test/html/core.html +27 -27
  102. package/test/stub.test.ts +7 -7
  103. package/test/tsconfig.test.json +5 -5
  104. package/tsconfig.json +21 -21
  105. package/tslint.json +32 -32
  106. package/types/agent.d.ts +39 -39
  107. package/types/core.d.ts +150 -150
  108. package/types/data.d.ts +572 -571
  109. package/types/diagnostic.d.ts +24 -24
  110. package/types/global.d.ts +30 -30
  111. package/types/index.d.ts +40 -40
  112. package/types/interaction.d.ts +177 -177
  113. package/types/layout.d.ts +276 -276
  114. package/types/performance.d.ts +31 -31
@@ -1,412 +1,412 @@
1
- import { Priority, Task, Timer } from "@clarity-types/core";
2
- import { Code, Event, Metric, Severity } from "@clarity-types/data";
3
- import { Constant, MutationHistory, MutationRecordWithTime, MutationQueue, Setting, Source } from "@clarity-types/layout";
4
- import api from "@src/core/api";
5
- import * as core from "@src/core";
6
- import * as event from "@src/core/event";
7
- import measure from "@src/core/measure";
8
- import * as task from "@src/core/task";
9
- import { time } from "@src/core/time";
10
- import { clearTimeout, setTimeout } from "@src/core/timeout";
11
- import { id } from "@src/data/metadata";
12
- import * as summary from "@src/data/summary";
13
- import * as internal from "@src/diagnostic/internal";
14
- import * as doc from "@src/layout/document";
15
- import * as dom from "@src/layout/dom";
16
- import * as metric from "@src/data/metric";
17
- import encode from "@src/layout/encode";
18
- import * as region from "@src/layout/region";
19
- import traverse from "@src/layout/traverse";
20
- import processNode from "./node";
21
- import config from "@src/core/config";
22
-
23
- let observers: Set<MutationObserver> = new Set();
24
- let mutations: MutationQueue[] = [];
25
- let throttledMutations: { [key: number]: MutationRecordWithTime } = {};
26
- let queue: Node[] = [];
27
- let timeout: number = null;
28
- let throttleDelay: number = null;
29
- let activePeriod = null;
30
- let history: MutationHistory = {};
31
- let observedNodes: WeakMap<Node, MutationObserver> = new WeakMap<Node, MutationObserver>();
32
-
33
- // We ignore mutations if these attributes are updated
34
- const IGNORED_ATTRIBUTES = ["data-google-query-id", "data-load-complete", "data-google-container-id"];
35
-
36
- export function start(): void {
37
- observers = new Set();
38
- queue = [];
39
- timeout = null;
40
- activePeriod = 0;
41
- history = {};
42
- observedNodes = new WeakMap<Node, MutationObserver>();
43
- proxyStyleRules(window);
44
- }
45
-
46
- export function observe(node: Document | ShadowRoot): void {
47
- // Create a new observer for every time a new DOM tree (e.g. root document or shadowdom root) is discovered on the page
48
- // In the case of shadow dom, any mutations that happen within the shadow dom are not bubbled up to the host document
49
- // For this reason, we need to wire up mutations every time we see a new shadow dom.
50
- // Also, wrap it inside a try / catch. In certain browsers (e.g. legacy Edge), observer on shadow dom can throw errors
51
- try {
52
-
53
- let m = api(Constant.MutationObserver);
54
- let observer = m in window ? new window[m](measure(handle) as MutationCallback) : null;
55
- if (observer) {
56
- observer.observe(node, { attributes: true, childList: true, characterData: true, subtree: true });
57
- observedNodes.set(node, observer);
58
- observers.add(observer);
59
- }
60
- if (node['defaultView']) {
61
- proxyStyleRules(node['defaultView']);
62
- }
63
-
64
- } catch (e) {
65
- internal.log(Code.MutationObserver, Severity.Info, e ? e.name : null);
66
- }
67
- }
68
-
69
- export function monitor(frame: HTMLIFrameElement): void {
70
- // Bind to iframe's onload event so we get notified anytime there's an update to iframe content.
71
- // This includes cases where iframe location is updated without explicitly updating src attribute
72
- // E.g. iframe.contentWindow.location.href = "new-location";
73
- if (dom.has(frame) === false) {
74
- event.bind(frame, Constant.LoadEvent, generate.bind(this, frame, Constant.ChildList), true);
75
- }
76
- }
77
-
78
- export function stop(): void {
79
- for (let observer of Array.from(observers)) {
80
- if (observer) {
81
- observer.disconnect();
82
- }
83
- }
84
- observers = new Set();
85
- history = {};
86
- mutations = [];
87
- throttledMutations = {};
88
- queue = [];
89
- activePeriod = 0;
90
- timeout = null;
91
- observedNodes = new WeakMap();
92
- }
93
-
94
- export function active(): void {
95
- activePeriod = time() + Setting.MutationActivePeriod;
96
- }
97
-
98
- export function disconnect(n: Node): void {
99
- const ob = observedNodes.get(n);
100
- if (ob) {
101
- ob.disconnect();
102
- observers.delete(ob);
103
- observedNodes.delete(n);
104
- }
105
- }
106
-
107
- function handle(m: MutationRecord[]): void {
108
- // Queue up mutation records for asynchronous processing
109
- let now = time();
110
- summary.track(Event.Mutation, now);
111
- mutations.push({ time: now, mutations: m });
112
- task.schedule(process, Priority.High).then((): void => {
113
- setTimeout(doc.compute);
114
- measure(region.compute)();
115
- });
116
- }
117
-
118
- async function processMutation(timer: Timer, mutation: MutationRecord, instance: number, timestamp: number): Promise<void> {
119
- let state = task.state(timer);
120
-
121
- if (state === Task.Wait) {
122
- state = await task.suspend(timer);
123
- }
124
- if (state === Task.Stop) {
125
- return;
126
- }
127
-
128
- let target = mutation.target;
129
- let type = config.throttleDom ? track(mutation, timer, instance, timestamp) : mutation.type;
130
-
131
- if (type && target && target.ownerDocument) {
132
- dom.parse(target.ownerDocument);
133
- }
134
-
135
- if (type && target && target.nodeType == Node.DOCUMENT_FRAGMENT_NODE && (target as ShadowRoot).host) {
136
- dom.parse(target as ShadowRoot);
137
- }
138
-
139
- switch (type) {
140
- case Constant.Attributes:
141
- if (IGNORED_ATTRIBUTES.indexOf(mutation.attributeName) < 0) {
142
- processNode(target, Source.Attributes, timestamp);
143
- }
144
- break;
145
- case Constant.CharacterData:
146
- processNode(target, Source.CharacterData, timestamp);
147
- break;
148
- case Constant.ChildList:
149
- processNodeList(mutation.addedNodes, Source.ChildListAdd, timer, timestamp);
150
- processNodeList(mutation.removedNodes, Source.ChildListRemove, timer, timestamp);
151
- break;
152
- case Constant.Throttle:
153
- default:
154
- break;
155
- }
156
- }
157
- async function process(): Promise<void> {
158
- let timer: Timer = { id: id(), cost: Metric.LayoutCost };
159
- task.start(timer);
160
- while (mutations.length > 0) {
161
- let record = mutations.shift();
162
- let instance = time();
163
- for (let mutation of record.mutations) {
164
- await processMutation(timer, mutation, instance, record.time);
165
- }
166
- await encode(Event.Mutation, timer, record.time);
167
- }
168
-
169
- let processedMutations = false;
170
- for (var key of Object.keys(throttledMutations)) {
171
- let throttledMutationToProcess: MutationRecordWithTime = throttledMutations[key];
172
- delete throttledMutations[key];
173
- await processMutation(timer, throttledMutationToProcess.mutation, time(), throttledMutationToProcess.timestamp);
174
- processedMutations = true;
175
- }
176
-
177
- if (Object.keys(throttledMutations).length > 0) {
178
- processThrottledMutations();
179
- }
180
-
181
- // ensure we encode the previously throttled mutations once we have finished them
182
- if (Object.keys(throttledMutations).length === 0 && processedMutations) {
183
- await encode(Event.Mutation, timer, time());
184
- }
185
-
186
- cleanHistory();
187
-
188
- task.stop(timer);
189
- }
190
-
191
- function cleanHistory(): void {
192
- let now = time();
193
- if (Object.keys(history).length > Setting.MaxMutationHistoryCount) {
194
- history = {};
195
- metric.count(Metric.HistoryClear);
196
- }
197
-
198
- for (let key of Object.keys(history)) {
199
- let h = history[key];
200
- if (now > h[1] + Setting.MaxMutationHistoryTime) {
201
- delete history[key];
202
- }
203
- }
204
- }
205
-
206
- function track(m: MutationRecord, timer: Timer, instance: number, timestamp: number): string {
207
- let value = m.target ? dom.get(m.target.parentNode) : null;
208
-
209
- // Check if the parent is already discovered and that the parent is not the document root
210
- if (value && value.data.tag !== Constant.HTML) {
211
- // calculate inactive period based on the timestamp of the mutation not when the mutation is processed
212
- let inactive = timestamp > activePeriod;
213
-
214
- // Calculate critical period based on when mutation is processed
215
- let target = dom.get(m.target);
216
- let element = target && target.selector ? target.selector.join() : m.target.nodeName;
217
- let parent = value.selector ? value.selector.join() : Constant.Empty;
218
-
219
- // We use selector, instead of id, to determine the key (signature for the mutation) because in some cases
220
- // repeated mutations can cause elements to be destroyed and then recreated as new DOM nodes
221
- // In those cases, IDs will change however the selector (which is relative to DOM xPath) remains the same
222
- let key = [parent, element, m.attributeName, names(m.addedNodes), names(m.removedNodes)].join();
223
-
224
- // Initialize an entry if it doesn't already exist
225
- history[key] = key in history ? history[key] : [0, instance];
226
- let h = history[key];
227
-
228
- // Lookup any pending nodes queued up for removal, and process them now if we suspended a mutation before
229
- if (inactive === false && h[0] >= Setting.MutationSuspendThreshold) {
230
- processNodeList(h[2], Source.ChildListRemove, timer, timestamp);
231
- }
232
-
233
- // Update the counter, do not reset counter if its critical period
234
- h[0] = inactive ? (h[1] === instance ? h[0] : h[0] + 1) : 1;
235
- h[1] = instance;
236
-
237
- // Return updated mutation type based on,
238
- // 1. if we have already hit the threshold or not
239
- // 2. if its a low priority mutation happening during critical time period
240
- if (h[0] >= Setting.MutationSuspendThreshold) {
241
- // Store a reference to removedNodes so we can process them later
242
- // when we resume mutations again on user interactions
243
- h[2] = m.removedNodes;
244
- if (instance > timestamp + Setting.MutationActivePeriod) {
245
- return m.type;
246
- }
247
-
248
- // we only store the most recent mutation for a given key if it is being throttled
249
- throttledMutations[key] = { mutation: m, timestamp };
250
-
251
- return Constant.Throttle;
252
- }
253
- }
254
- return m.type;
255
- }
256
-
257
- function names(nodes: NodeList): string {
258
- let output: string[] = [];
259
- for (let i = 0; nodes && i < nodes.length; i++) {
260
- output.push(nodes[i].nodeName);
261
- }
262
- return output.join();
263
- }
264
-
265
- async function processNodeList(list: NodeList, source: Source, timer: Timer, timestamp: number): Promise<void> {
266
- let length = list ? list.length : 0;
267
- for (let i = 0; i < length; i++) {
268
- const node = list[i];
269
- if (source === Source.ChildListAdd) {
270
- traverse(node, timer, source, timestamp);
271
- } else {
272
- let state = task.state(timer);
273
- if (state === Task.Wait) {
274
- state = await task.suspend(timer);
275
- }
276
- if (state === Task.Stop) {
277
- break;
278
- }
279
- processNode(node, source, timestamp);
280
- }
281
- }
282
- }
283
-
284
- function processThrottledMutations(): void {
285
- if (throttleDelay) {
286
- clearTimeout(throttleDelay);
287
- }
288
- throttleDelay = setTimeout(() => {
289
- task.schedule(process, Priority.High);
290
- }, Setting.LookAhead);
291
- }
292
-
293
- export function schedule(node: Node): Node {
294
- // Only schedule manual trigger for this node if it's not already in the queue
295
- if (queue.indexOf(node) < 0) {
296
- queue.push(node);
297
- }
298
-
299
- // Cancel any previous trigger before scheduling a new one.
300
- // It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
301
- // And in those cases we do not wish to monitor changes multiple times for the same node.
302
- if (timeout) {
303
- clearTimeout(timeout);
304
- }
305
- timeout = setTimeout(() => {
306
- trigger();
307
- }, Setting.LookAhead);
308
-
309
- return node;
310
- }
311
-
312
- function trigger(): void {
313
- for (let node of queue) {
314
- // Generate a mutation for this node only if it still exists
315
- if (node) {
316
- let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
317
- // Skip re-processing shadowRoot if it was already discovered
318
- if (shadowRoot && dom.has(node)) {
319
- continue;
320
- }
321
- generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
322
- }
323
- }
324
- queue = [];
325
- }
326
-
327
- function generate(target: Node, type: MutationRecordType): void {
328
- measure(handle)([
329
- {
330
- addedNodes: [target],
331
- attributeName: null,
332
- attributeNamespace: null,
333
- nextSibling: null,
334
- oldValue: null,
335
- previousSibling: null,
336
- removedNodes: [],
337
- target,
338
- type,
339
- },
340
- ]);
341
- }
342
-
343
-
344
- function proxyStyleRules(win: any): void {
345
- if (win === null || win === undefined) {
346
- return;
347
- }
348
-
349
- win.clarityOverrides = win.clarityOverrides || {};
350
-
351
- // Some popular open source libraries, like styled-components, optimize performance
352
- // by injecting CSS using insertRule API vs. appending text node. A side effect of
353
- // using javascript API is that it doesn't trigger DOM mutation and therefore we
354
- // need to override the insertRule API and listen for changes manually.
355
- if (win.clarityOverrides.InsertRule === undefined) {
356
- win.clarityOverrides.InsertRule = win.CSSStyleSheet.prototype.insertRule;
357
- win.CSSStyleSheet.prototype.insertRule = function (): number {
358
- if (core.active()) {
359
- schedule(this.ownerNode);
360
- }
361
- return win.clarityOverrides.InsertRule.apply(this, arguments);
362
- };
363
- }
364
-
365
- if ("CSSMediaRule" in win && win.clarityOverrides.MediaInsertRule === undefined) {
366
- win.clarityOverrides.MediaInsertRule = win.CSSMediaRule.prototype.insertRule;
367
- win.CSSMediaRule.prototype.insertRule = function (): number {
368
- if (core.active()) {
369
- schedule(this.parentStyleSheet.ownerNode);
370
- }
371
- return win.clarityOverrides.MediaInsertRule.apply(this, arguments);
372
- };
373
- }
374
-
375
- if (win.clarityOverrides.DeleteRule === undefined) {
376
- win.clarityOverrides.DeleteRule = win.CSSStyleSheet.prototype.deleteRule;
377
- win.CSSStyleSheet.prototype.deleteRule = function (): void {
378
- if (core.active()) {
379
- schedule(this.ownerNode);
380
- }
381
- return win.clarityOverrides.DeleteRule.apply(this, arguments);
382
- };
383
- }
384
-
385
- if ("CSSMediaRule" in win && win.clarityOverrides.MediaDeleteRule === undefined) {
386
- win.clarityOverrides.MediaDeleteRule = win.CSSMediaRule.prototype.deleteRule;
387
- win.CSSMediaRule.prototype.deleteRule = function (): void {
388
- if (core.active()) {
389
- schedule(this.parentStyleSheet.ownerNode);
390
- }
391
- return win.clarityOverrides.MediaDeleteRule.apply(this, arguments);
392
- };
393
- }
394
-
395
- // Add a hook to attachShadow API calls
396
- // In case we are unable to add a hook and browser throws an exception,
397
- // reset attachShadow variable and resume processing like before
398
- if (win.clarityOverrides.AttachShadow === undefined) {
399
- win.clarityOverrides.AttachShadow = win.Element.prototype.attachShadow;
400
- try {
401
- win.Element.prototype.attachShadow = function (): ShadowRoot {
402
- if (core.active()) {
403
- return schedule(win.clarityOverrides.AttachShadow.apply(this, arguments)) as ShadowRoot;
404
- } else {
405
- return win.clarityOverrides.AttachShadow.apply(this, arguments);
406
- }
407
- };
408
- } catch {
409
- win.clarityOverrides.AttachShadow = null;
410
- }
411
- }
1
+ import { Priority, Task, Timer } from "@clarity-types/core";
2
+ import { Code, Event, Metric, Severity } from "@clarity-types/data";
3
+ import { Constant, MutationHistory, MutationRecordWithTime, MutationQueue, Setting, Source } from "@clarity-types/layout";
4
+ import api from "@src/core/api";
5
+ import * as core from "@src/core";
6
+ import * as event from "@src/core/event";
7
+ import measure from "@src/core/measure";
8
+ import * as task from "@src/core/task";
9
+ import { time } from "@src/core/time";
10
+ import { clearTimeout, setTimeout } from "@src/core/timeout";
11
+ import { id } from "@src/data/metadata";
12
+ import * as summary from "@src/data/summary";
13
+ import * as internal from "@src/diagnostic/internal";
14
+ import * as doc from "@src/layout/document";
15
+ import * as dom from "@src/layout/dom";
16
+ import * as metric from "@src/data/metric";
17
+ import encode from "@src/layout/encode";
18
+ import * as region from "@src/layout/region";
19
+ import traverse from "@src/layout/traverse";
20
+ import processNode from "./node";
21
+ import config from "@src/core/config";
22
+
23
+ let observers: Set<MutationObserver> = new Set();
24
+ let mutations: MutationQueue[] = [];
25
+ let throttledMutations: { [key: number]: MutationRecordWithTime } = {};
26
+ let queue: Node[] = [];
27
+ let timeout: number = null;
28
+ let throttleDelay: number = null;
29
+ let activePeriod = null;
30
+ let history: MutationHistory = {};
31
+ let observedNodes: WeakMap<Node, MutationObserver> = new WeakMap<Node, MutationObserver>();
32
+
33
+ // We ignore mutations if these attributes are updated
34
+ const IGNORED_ATTRIBUTES = ["data-google-query-id", "data-load-complete", "data-google-container-id"];
35
+
36
+ export function start(): void {
37
+ observers = new Set();
38
+ queue = [];
39
+ timeout = null;
40
+ activePeriod = 0;
41
+ history = {};
42
+ observedNodes = new WeakMap<Node, MutationObserver>();
43
+ proxyStyleRules(window);
44
+ }
45
+
46
+ export function observe(node: Document | ShadowRoot): void {
47
+ // Create a new observer for every time a new DOM tree (e.g. root document or shadowdom root) is discovered on the page
48
+ // In the case of shadow dom, any mutations that happen within the shadow dom are not bubbled up to the host document
49
+ // For this reason, we need to wire up mutations every time we see a new shadow dom.
50
+ // Also, wrap it inside a try / catch. In certain browsers (e.g. legacy Edge), observer on shadow dom can throw errors
51
+ try {
52
+
53
+ let m = api(Constant.MutationObserver);
54
+ let observer = m in window ? new window[m](measure(handle) as MutationCallback) : null;
55
+ if (observer) {
56
+ observer.observe(node, { attributes: true, childList: true, characterData: true, subtree: true });
57
+ observedNodes.set(node, observer);
58
+ observers.add(observer);
59
+ }
60
+ if (node['defaultView']) {
61
+ proxyStyleRules(node['defaultView']);
62
+ }
63
+
64
+ } catch (e) {
65
+ internal.log(Code.MutationObserver, Severity.Info, e ? e.name : null);
66
+ }
67
+ }
68
+
69
+ export function monitor(frame: HTMLIFrameElement): void {
70
+ // Bind to iframe's onload event so we get notified anytime there's an update to iframe content.
71
+ // This includes cases where iframe location is updated without explicitly updating src attribute
72
+ // E.g. iframe.contentWindow.location.href = "new-location";
73
+ if (dom.has(frame) === false) {
74
+ event.bind(frame, Constant.LoadEvent, generate.bind(this, frame, Constant.ChildList), true);
75
+ }
76
+ }
77
+
78
+ export function stop(): void {
79
+ for (let observer of Array.from(observers)) {
80
+ if (observer) {
81
+ observer.disconnect();
82
+ }
83
+ }
84
+ observers = new Set();
85
+ history = {};
86
+ mutations = [];
87
+ throttledMutations = {};
88
+ queue = [];
89
+ activePeriod = 0;
90
+ timeout = null;
91
+ observedNodes = new WeakMap();
92
+ }
93
+
94
+ export function active(): void {
95
+ activePeriod = time() + Setting.MutationActivePeriod;
96
+ }
97
+
98
+ export function disconnect(n: Node): void {
99
+ const ob = observedNodes.get(n);
100
+ if (ob) {
101
+ ob.disconnect();
102
+ observers.delete(ob);
103
+ observedNodes.delete(n);
104
+ }
105
+ }
106
+
107
+ function handle(m: MutationRecord[]): void {
108
+ // Queue up mutation records for asynchronous processing
109
+ let now = time();
110
+ summary.track(Event.Mutation, now);
111
+ mutations.push({ time: now, mutations: m });
112
+ task.schedule(process, Priority.High).then((): void => {
113
+ setTimeout(doc.compute);
114
+ measure(region.compute)();
115
+ });
116
+ }
117
+
118
+ async function processMutation(timer: Timer, mutation: MutationRecord, instance: number, timestamp: number): Promise<void> {
119
+ let state = task.state(timer);
120
+
121
+ if (state === Task.Wait) {
122
+ state = await task.suspend(timer);
123
+ }
124
+ if (state === Task.Stop) {
125
+ return;
126
+ }
127
+
128
+ let target = mutation.target;
129
+ let type = config.throttleDom ? track(mutation, timer, instance, timestamp) : mutation.type;
130
+
131
+ if (type && target && target.ownerDocument) {
132
+ dom.parse(target.ownerDocument);
133
+ }
134
+
135
+ if (type && target && target.nodeType == Node.DOCUMENT_FRAGMENT_NODE && (target as ShadowRoot).host) {
136
+ dom.parse(target as ShadowRoot);
137
+ }
138
+
139
+ switch (type) {
140
+ case Constant.Attributes:
141
+ if (IGNORED_ATTRIBUTES.indexOf(mutation.attributeName) < 0) {
142
+ processNode(target, Source.Attributes, timestamp);
143
+ }
144
+ break;
145
+ case Constant.CharacterData:
146
+ processNode(target, Source.CharacterData, timestamp);
147
+ break;
148
+ case Constant.ChildList:
149
+ processNodeList(mutation.addedNodes, Source.ChildListAdd, timer, timestamp);
150
+ processNodeList(mutation.removedNodes, Source.ChildListRemove, timer, timestamp);
151
+ break;
152
+ case Constant.Throttle:
153
+ default:
154
+ break;
155
+ }
156
+ }
157
+ async function process(): Promise<void> {
158
+ let timer: Timer = { id: id(), cost: Metric.LayoutCost };
159
+ task.start(timer);
160
+ while (mutations.length > 0) {
161
+ let record = mutations.shift();
162
+ let instance = time();
163
+ for (let mutation of record.mutations) {
164
+ await processMutation(timer, mutation, instance, record.time);
165
+ }
166
+ await encode(Event.Mutation, timer, record.time);
167
+ }
168
+
169
+ let processedMutations = false;
170
+ for (var key of Object.keys(throttledMutations)) {
171
+ let throttledMutationToProcess: MutationRecordWithTime = throttledMutations[key];
172
+ delete throttledMutations[key];
173
+ await processMutation(timer, throttledMutationToProcess.mutation, time(), throttledMutationToProcess.timestamp);
174
+ processedMutations = true;
175
+ }
176
+
177
+ if (Object.keys(throttledMutations).length > 0) {
178
+ processThrottledMutations();
179
+ }
180
+
181
+ // ensure we encode the previously throttled mutations once we have finished them
182
+ if (Object.keys(throttledMutations).length === 0 && processedMutations) {
183
+ await encode(Event.Mutation, timer, time());
184
+ }
185
+
186
+ cleanHistory();
187
+
188
+ task.stop(timer);
189
+ }
190
+
191
+ function cleanHistory(): void {
192
+ let now = time();
193
+ if (Object.keys(history).length > Setting.MaxMutationHistoryCount) {
194
+ history = {};
195
+ metric.count(Metric.HistoryClear);
196
+ }
197
+
198
+ for (let key of Object.keys(history)) {
199
+ let h = history[key];
200
+ if (now > h[1] + Setting.MaxMutationHistoryTime) {
201
+ delete history[key];
202
+ }
203
+ }
204
+ }
205
+
206
+ function track(m: MutationRecord, timer: Timer, instance: number, timestamp: number): string {
207
+ let value = m.target ? dom.get(m.target.parentNode) : null;
208
+
209
+ // Check if the parent is already discovered and that the parent is not the document root
210
+ if (value && value.data.tag !== Constant.HTML) {
211
+ // calculate inactive period based on the timestamp of the mutation not when the mutation is processed
212
+ let inactive = timestamp > activePeriod;
213
+
214
+ // Calculate critical period based on when mutation is processed
215
+ let target = dom.get(m.target);
216
+ let element = target && target.selector ? target.selector.join() : m.target.nodeName;
217
+ let parent = value.selector ? value.selector.join() : Constant.Empty;
218
+
219
+ // We use selector, instead of id, to determine the key (signature for the mutation) because in some cases
220
+ // repeated mutations can cause elements to be destroyed and then recreated as new DOM nodes
221
+ // In those cases, IDs will change however the selector (which is relative to DOM xPath) remains the same
222
+ let key = [parent, element, m.attributeName, names(m.addedNodes), names(m.removedNodes)].join();
223
+
224
+ // Initialize an entry if it doesn't already exist
225
+ history[key] = key in history ? history[key] : [0, instance];
226
+ let h = history[key];
227
+
228
+ // Lookup any pending nodes queued up for removal, and process them now if we suspended a mutation before
229
+ if (inactive === false && h[0] >= Setting.MutationSuspendThreshold) {
230
+ processNodeList(h[2], Source.ChildListRemove, timer, timestamp);
231
+ }
232
+
233
+ // Update the counter, do not reset counter if its critical period
234
+ h[0] = inactive ? (h[1] === instance ? h[0] : h[0] + 1) : 1;
235
+ h[1] = instance;
236
+
237
+ // Return updated mutation type based on,
238
+ // 1. if we have already hit the threshold or not
239
+ // 2. if its a low priority mutation happening during critical time period
240
+ if (h[0] >= Setting.MutationSuspendThreshold) {
241
+ // Store a reference to removedNodes so we can process them later
242
+ // when we resume mutations again on user interactions
243
+ h[2] = m.removedNodes;
244
+ if (instance > timestamp + Setting.MutationActivePeriod) {
245
+ return m.type;
246
+ }
247
+
248
+ // we only store the most recent mutation for a given key if it is being throttled
249
+ throttledMutations[key] = { mutation: m, timestamp };
250
+
251
+ return Constant.Throttle;
252
+ }
253
+ }
254
+ return m.type;
255
+ }
256
+
257
+ function names(nodes: NodeList): string {
258
+ let output: string[] = [];
259
+ for (let i = 0; nodes && i < nodes.length; i++) {
260
+ output.push(nodes[i].nodeName);
261
+ }
262
+ return output.join();
263
+ }
264
+
265
+ async function processNodeList(list: NodeList, source: Source, timer: Timer, timestamp: number): Promise<void> {
266
+ let length = list ? list.length : 0;
267
+ for (let i = 0; i < length; i++) {
268
+ const node = list[i];
269
+ if (source === Source.ChildListAdd) {
270
+ traverse(node, timer, source, timestamp);
271
+ } else {
272
+ let state = task.state(timer);
273
+ if (state === Task.Wait) {
274
+ state = await task.suspend(timer);
275
+ }
276
+ if (state === Task.Stop) {
277
+ break;
278
+ }
279
+ processNode(node, source, timestamp);
280
+ }
281
+ }
282
+ }
283
+
284
+ function processThrottledMutations(): void {
285
+ if (throttleDelay) {
286
+ clearTimeout(throttleDelay);
287
+ }
288
+ throttleDelay = setTimeout(() => {
289
+ task.schedule(process, Priority.High);
290
+ }, Setting.LookAhead);
291
+ }
292
+
293
+ export function schedule(node: Node): Node {
294
+ // Only schedule manual trigger for this node if it's not already in the queue
295
+ if (queue.indexOf(node) < 0) {
296
+ queue.push(node);
297
+ }
298
+
299
+ // Cancel any previous trigger before scheduling a new one.
300
+ // It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
301
+ // And in those cases we do not wish to monitor changes multiple times for the same node.
302
+ if (timeout) {
303
+ clearTimeout(timeout);
304
+ }
305
+ timeout = setTimeout(() => {
306
+ trigger();
307
+ }, Setting.LookAhead);
308
+
309
+ return node;
310
+ }
311
+
312
+ function trigger(): void {
313
+ for (let node of queue) {
314
+ // Generate a mutation for this node only if it still exists
315
+ if (node) {
316
+ let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
317
+ // Skip re-processing shadowRoot if it was already discovered
318
+ if (shadowRoot && dom.has(node)) {
319
+ continue;
320
+ }
321
+ generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
322
+ }
323
+ }
324
+ queue = [];
325
+ }
326
+
327
+ function generate(target: Node, type: MutationRecordType): void {
328
+ measure(handle)([
329
+ {
330
+ addedNodes: [target],
331
+ attributeName: null,
332
+ attributeNamespace: null,
333
+ nextSibling: null,
334
+ oldValue: null,
335
+ previousSibling: null,
336
+ removedNodes: [],
337
+ target,
338
+ type,
339
+ },
340
+ ]);
341
+ }
342
+
343
+
344
+ function proxyStyleRules(win: any): void {
345
+ if (win === null || win === undefined) {
346
+ return;
347
+ }
348
+
349
+ win.clarityOverrides = win.clarityOverrides || {};
350
+
351
+ // Some popular open source libraries, like styled-components, optimize performance
352
+ // by injecting CSS using insertRule API vs. appending text node. A side effect of
353
+ // using javascript API is that it doesn't trigger DOM mutation and therefore we
354
+ // need to override the insertRule API and listen for changes manually.
355
+ if (win.clarityOverrides.InsertRule === undefined) {
356
+ win.clarityOverrides.InsertRule = win.CSSStyleSheet.prototype.insertRule;
357
+ win.CSSStyleSheet.prototype.insertRule = function (): number {
358
+ if (core.active()) {
359
+ schedule(this.ownerNode);
360
+ }
361
+ return win.clarityOverrides.InsertRule.apply(this, arguments);
362
+ };
363
+ }
364
+
365
+ if ("CSSMediaRule" in win && win.clarityOverrides.MediaInsertRule === undefined) {
366
+ win.clarityOverrides.MediaInsertRule = win.CSSMediaRule.prototype.insertRule;
367
+ win.CSSMediaRule.prototype.insertRule = function (): number {
368
+ if (core.active()) {
369
+ schedule(this.parentStyleSheet.ownerNode);
370
+ }
371
+ return win.clarityOverrides.MediaInsertRule.apply(this, arguments);
372
+ };
373
+ }
374
+
375
+ if (win.clarityOverrides.DeleteRule === undefined) {
376
+ win.clarityOverrides.DeleteRule = win.CSSStyleSheet.prototype.deleteRule;
377
+ win.CSSStyleSheet.prototype.deleteRule = function (): void {
378
+ if (core.active()) {
379
+ schedule(this.ownerNode);
380
+ }
381
+ return win.clarityOverrides.DeleteRule.apply(this, arguments);
382
+ };
383
+ }
384
+
385
+ if ("CSSMediaRule" in win && win.clarityOverrides.MediaDeleteRule === undefined) {
386
+ win.clarityOverrides.MediaDeleteRule = win.CSSMediaRule.prototype.deleteRule;
387
+ win.CSSMediaRule.prototype.deleteRule = function (): void {
388
+ if (core.active()) {
389
+ schedule(this.parentStyleSheet.ownerNode);
390
+ }
391
+ return win.clarityOverrides.MediaDeleteRule.apply(this, arguments);
392
+ };
393
+ }
394
+
395
+ // Add a hook to attachShadow API calls
396
+ // In case we are unable to add a hook and browser throws an exception,
397
+ // reset attachShadow variable and resume processing like before
398
+ if (win.clarityOverrides.AttachShadow === undefined) {
399
+ win.clarityOverrides.AttachShadow = win.Element.prototype.attachShadow;
400
+ try {
401
+ win.Element.prototype.attachShadow = function (): ShadowRoot {
402
+ if (core.active()) {
403
+ return schedule(win.clarityOverrides.AttachShadow.apply(this, arguments)) as ShadowRoot;
404
+ } else {
405
+ return win.clarityOverrides.AttachShadow.apply(this, arguments);
406
+ }
407
+ };
408
+ } catch {
409
+ win.clarityOverrides.AttachShadow = null;
410
+ }
411
+ }
412
412
  }