From 552db01f3c73d1367d38343100ff15f43c881bc2 Mon Sep 17 00:00:00 2001 From: Manu Nair Date: Sun, 29 Mar 2026 16:23:02 -0700 Subject: [PATCH 1/8] build: optimize terser config and rollup output options Add shared terser config with passes:3, hoist_vars:true for better gzip. Add freeze:false on all IIFE outputs to remove Object.freeze() wrapper. --- packages/clarity-js/rollup.config.ts | 37 +++++++++++++++++----------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/clarity-js/rollup.config.ts b/packages/clarity-js/rollup.config.ts index 66a49054..6c6ea903 100644 --- a/packages/clarity-js/rollup.config.ts +++ b/packages/clarity-js/rollup.config.ts @@ -6,6 +6,15 @@ import typescript from "@rollup/plugin-typescript"; import { readFileSync } from "fs"; const pkg = JSON.parse(readFileSync("./package.json", "utf-8")); +const terserOpts = { + compress: { + passes: 3, + hoist_vars: true, + }, + mangle: { + }, + output: { comments: false } +}; export default [ { input: "src/index.ts", @@ -25,7 +34,7 @@ export default [ }, { input: "src/global.ts", - output: [ { file: pkg.unpkg, format: "iife", exports: "named" } ], + output: [ { file: pkg.unpkg, format: "iife", exports: "named", freeze: false } ], onwarn(message, warn) { if (message.code === 'CIRCULAR_DEPENDENCY') { return; } warn(message); @@ -33,13 +42,13 @@ export default [ plugins: [ resolve(), typescript(), - terser({output: {comments: false}}), + terser(terserOpts), commonjs({ include: ["node_modules/**"] }) ] }, { input: "src/global.ts", - output: [ { file: pkg.extended, format: "iife", exports: "named" } ], + output: [ { file: pkg.extended, format: "iife", exports: "named", freeze: false } ], onwarn(message, warn) { if (message.code === 'CIRCULAR_DEPENDENCY') { return; } warn(message); @@ -47,13 +56,13 @@ export default [ plugins: [ resolve(), typescript(), - terser({output: {comments: false}}), + terser(terserOpts), commonjs({ include: ["node_modules/**"] }) ] }, { input: "src/global.ts", - output: [ { file: pkg.insight, format: "iife", exports: "named" } ], + output: [ { file: pkg.insight, format: "iife", exports: "named", freeze: false } ], onwarn(message, warn) { if (message.code === 'CIRCULAR_DEPENDENCY') { return; } warn(message); @@ -71,13 +80,13 @@ export default [ }), resolve(), typescript(), - terser({output: {comments: false}}), + terser(terserOpts), commonjs({ include: ["node_modules/**"] }) ] }, { input: "src/global.ts", - output: [ { file: pkg.performance, format: "iife", exports: "named" } ], + output: [ { file: pkg.performance, format: "iife", exports: "named", freeze: false } ], onwarn(message, warn) { if (message.code === 'CIRCULAR_DEPENDENCY') { return; } warn(message); @@ -94,13 +103,13 @@ export default [ }), resolve(), typescript(), - terser({output: {comments: false}}), + terser(terserOpts), commonjs({ include: ["node_modules/**"] }) ] }, { input: "src/dynamic/agent/index.ts", - output: [ { file: pkg.livechat, format: "iife", exports: "named" } ], + output: [ { file: pkg.livechat, format: "iife", exports: "named", freeze: false } ], onwarn(message, warn) { if (message.code === 'CIRCULAR_DEPENDENCY') { return; } warn(message); @@ -114,13 +123,13 @@ export default [ }), resolve(), typescript(), - terser({output: {comments: false}}), + terser(terserOpts), commonjs({ include: ["node_modules/**"] }) ] }, { input: "src/dynamic/agent/index.ts", - output: [ { file: pkg.tidio, format: "iife", exports: "named" } ], + output: [ { file: pkg.tidio, format: "iife", exports: "named", freeze: false } ], onwarn(message, warn) { if (message.code === 'CIRCULAR_DEPENDENCY') { return; } warn(message); @@ -134,13 +143,13 @@ export default [ }), resolve(), typescript(), - terser({output: {comments: false}}), + terser(terserOpts), commonjs({ include: ["node_modules/**"] }) ] }, { input: "src/dynamic/agent/index.ts", - output: [ { file: pkg.crisp, format: "iife", exports: "named" } ], + output: [ { file: pkg.crisp, format: "iife", exports: "named", freeze: false } ], onwarn(message, warn) { if (message.code === 'CIRCULAR_DEPENDENCY') { return; } warn(message); @@ -154,7 +163,7 @@ export default [ }), resolve(), typescript(), - terser({output: {comments: false}}), + terser(terserOpts), commonjs({ include: ["node_modules/**"] }) ] } From c4b43f0cba6209f00a0f88f964e1c52e347e88c1 Mon Sep 17 00:00:00 2001 From: Manu Nair Date: Sun, 29 Mar 2026 16:23:02 -0700 Subject: [PATCH 2/8] refactor: eliminate rollup namespace objects via named imports Replace import * as with named imports {start, stop} in module arrays across clarity.ts, data/index.ts, and all sub-module index files. Eliminate dom[] dynamic dispatch in node.ts with domCall helper. Removes rollup namespace objects that contained ALL module exports when only {start, stop} was needed. --- packages/clarity-js/src/clarity.ts | 18 ++-- packages/clarity-js/src/data/index.ts | 61 ++++++----- packages/clarity-js/src/diagnostic/index.ts | 14 +-- packages/clarity-js/src/interaction/index.ts | 102 +++++++++---------- packages/clarity-js/src/layout/index.ts | 53 +++++----- packages/clarity-js/src/layout/node.ts | 61 +++++------ packages/clarity-js/src/performance/index.ts | 12 +-- 7 files changed, 166 insertions(+), 155 deletions(-) diff --git a/packages/clarity-js/src/clarity.ts b/packages/clarity-js/src/clarity.ts index 0ebc37a4..3db6c86d 100644 --- a/packages/clarity-js/src/clarity.ts +++ b/packages/clarity-js/src/clarity.ts @@ -6,11 +6,11 @@ import measure from "@src/core/measure"; import * as task from "@src/core/task"; import version from "@src/core/version"; import * as data from "@src/data"; -import * as diagnostic from "@src/diagnostic"; -import * as interaction from "@src/interaction"; -import * as layout from "@src/layout"; -import * as performance from "@src/performance"; -import * as dynamic from "@src/core/dynamic"; +import { start as diagStart, stop as diagStop } from "@src/diagnostic"; +import { start as interStart, stop as interStop } from "@src/interaction"; +import { start as layoutStart, stop as layoutStop } from "@src/layout"; +import { start as perfStart, stop as perfStop } from "@src/performance"; +import { start as dynStart, stop as dynStop } from "@src/core/dynamic"; export { version }; export { consent, consentv2, event, identify, set, upgrade, metadata, signal, maxMetric, dlog } from "@src/data"; export { queue } from "@src/data/upload"; @@ -19,7 +19,13 @@ export { schedule } from "@src/core/task"; export { time } from "@src/core/time"; export { hashText } from "@src/layout"; export { measure }; -const modules: Module[] = [diagnostic, layout, interaction, performance, dynamic]; +const modules: Module[] = [ + { start: diagStart, stop: diagStop }, + { start: layoutStart, stop: layoutStop }, + { start: interStart, stop: interStop }, + { start: perfStart, stop: perfStop }, + { start: dynStart, stop: dynStop }, +]; export function start(config: Config = null): void { // Check that browser supports required APIs and we do not attempt to start Clarity multiple times diff --git a/packages/clarity-js/src/data/index.ts b/packages/clarity-js/src/data/index.ts index 8fe984b6..9c3da88e 100644 --- a/packages/clarity-js/src/data/index.ts +++ b/packages/clarity-js/src/data/index.ts @@ -1,19 +1,19 @@ import measure from "@src/core/measure"; -import * as baseline from "@src/data/baseline"; -import * as consent from "@src/data/consent"; -import * as envelope from "@src/data/envelope"; -import * as dimension from "@src/data/dimension"; -import * as metadata from "@src/data/metadata"; +import { start as baseStart, stop as baseStop, compute as baseCompute } from "@src/data/baseline"; +import { start as conStart, stop as conStop, compute as conCompute } from "@src/data/consent"; +import { start as envStart, stop as envStop } from "@src/data/envelope"; +import { start as dimStart, stop as dimStop, compute as dimCompute } from "@src/data/dimension"; +import { start as metaStart, stop as metaStop } from "@src/data/metadata"; import { Module } from "@clarity-types/core"; import * as metric from "@src/data/metric"; -import * as ping from "@src/data/ping"; -import * as limit from "@src/data/limit"; -import * as summary from "@src/data/summary"; -import * as upgrade from "@src/data/upgrade"; -import * as upload from "@src/data/upload"; -import * as variable from "@src/data/variable"; -import * as extract from "@src/data/extract"; -import * as cookie from "@src/data/cookie"; +import { start as pingStart, stop as pingStop } from "@src/data/ping"; +import { start as limStart, stop as limStop, compute as limCompute } from "@src/data/limit"; +import { start as sumStart, stop as sumStop, compute as sumCompute } from "@src/data/summary"; +import { start as upgrStart, stop as upgrStop } from "@src/data/upgrade"; +import { start as uplStart, stop as uplStop } from "@src/data/upload"; +import { start as varStart, stop as varStop, compute as varCompute } from "@src/data/variable"; +import { start as extStart, stop as extStop, compute as extCompute } from "@src/data/extract"; +import { start as cookStart, stop as cookStop } from "@src/data/cookie"; export { event } from "@src/data/custom"; export { consent, consentv2, metadata } from "@src/data/metadata"; export { upgrade } from "@src/data/upgrade"; @@ -22,30 +22,39 @@ export { signal } from "@src/data/signal"; export { max as maxMetric } from "@src/data/metric"; export { log as dlog } from "@src/data/dimension"; -const modules: Module[] = [baseline, dimension, variable, limit, summary, cookie, consent, metadata, envelope, upload, ping, upgrade, extract]; +const modules: Module[] = [ + { start: baseStart, stop: baseStop }, + { start: dimStart, stop: dimStop }, + { start: varStart, stop: varStop }, + { start: limStart, stop: limStop }, + { start: sumStart, stop: sumStop }, + { start: cookStart, stop: cookStop }, + { start: conStart, stop: conStop }, + { start: metaStart, stop: metaStop }, + { start: envStart, stop: envStop }, + { start: uplStart, stop: uplStop }, + { start: pingStart, stop: pingStop }, + { start: upgrStart, stop: upgrStop }, + { start: extStart, stop: extStop }, +]; export function start(): void { - // Metric needs to be initialized before we can start measuring. so metric is not wrapped in measure metric.start(); modules.forEach(x => measure(x.start)()); } export function stop(): void { - // Stop modules in the reverse order of their initialization - // The ordering below should respect inter-module dependency. - // E.g. if upgrade depends on upload, then upgrade needs to end before upload. - // Similarly, if upload depends on metadata, upload needs to end before metadata. modules.slice().reverse().forEach(x => measure(x.stop)()); metric.stop(); } export function compute(): void { - variable.compute(); - baseline.compute(); - dimension.compute(); + varCompute(); + baseCompute(); + dimCompute(); metric.compute(); - summary.compute(); - limit.compute(); - extract.compute(); - consent.compute(); + sumCompute(); + limCompute(); + extCompute(); + conCompute(); } diff --git a/packages/clarity-js/src/diagnostic/index.ts b/packages/clarity-js/src/diagnostic/index.ts index 979d4efe..185809cb 100644 --- a/packages/clarity-js/src/diagnostic/index.ts +++ b/packages/clarity-js/src/diagnostic/index.ts @@ -1,13 +1,13 @@ -import * as fraud from "@src/diagnostic/fraud"; -import * as internal from "@src/diagnostic/internal"; -import * as script from "@src/diagnostic/script"; +import { start as fraudStart } from "@src/diagnostic/fraud"; +import { start as intStart, stop as intStop } from "@src/diagnostic/internal"; +import { start as scrStart } from "@src/diagnostic/script"; export function start(): void { - fraud.start(); - script.start(); - internal.start(); + fraudStart(); + scrStart(); + intStart(); } export function stop(): void { - internal.stop(); + intStop(); } diff --git a/packages/clarity-js/src/interaction/index.ts b/packages/clarity-js/src/interaction/index.ts index d8823ab2..9cc2deda 100644 --- a/packages/clarity-js/src/interaction/index.ts +++ b/packages/clarity-js/src/interaction/index.ts @@ -1,63 +1,61 @@ -import * as change from "@src/interaction/change"; -import * as click from "@src/interaction/click"; -import * as clipboard from "@src/interaction/clipboard"; -import * as input from "@src/interaction/input"; -import * as pointer from "@src/interaction/pointer"; -import * as resize from "@src/interaction/resize"; -import * as scroll from "@src/interaction/scroll"; -import * as selection from "@src/interaction/selection"; -import * as submit from "@src/interaction/submit"; -import * as timeline from "@src/interaction/timeline"; -import * as unload from "@src/interaction/unload"; -import * as visibility from "@src/interaction/visibility"; -import * as focus from "@src/interaction/focus"; -import * as pageshow from "@src/interaction/pageshow"; +import { start as chgStart, stop as chgStop, observe as chgObs } from "@src/interaction/change"; +import { start as clkStart, stop as clkStop, observe as clkObs } from "@src/interaction/click"; +import { start as cbStart, stop as cbStop, observe as cbObs } from "@src/interaction/clipboard"; +import { start as inpStart, stop as inpStop, observe as inpObs } from "@src/interaction/input"; +import { start as ptrStart, stop as ptrStop, observe as ptrObs } from "@src/interaction/pointer"; +import { start as resStart, stop as resStop } from "@src/interaction/resize"; +import { start as scrStart, stop as scrStop, observe as scrObs } from "@src/interaction/scroll"; +import { start as selStart, stop as selStop, observe as selObs } from "@src/interaction/selection"; +import { start as subStart, stop as subStop, observe as subObs } from "@src/interaction/submit"; +import { start as tlStart, stop as tlStop } from "@src/interaction/timeline"; +import { start as unlStart, stop as unlStop } from "@src/interaction/unload"; +import { start as visStart, stop as visStop } from "@src/interaction/visibility"; +import { start as focStart, stop as focStop } from "@src/interaction/focus"; +import { start as pgStart, stop as pgStop } from "@src/interaction/pageshow"; export function start(): void { - timeline.start(); - click.start(); - clipboard.start(); - pointer.start(); - input.start(); - resize.start(); - visibility.start(); - focus.start(); - pageshow.start(); - scroll.start(); - selection.start(); - change.start(); - submit.start(); - unload.start(); + tlStart(); + clkStart(); + cbStart(); + ptrStart(); + inpStart(); + resStart(); + visStart(); + focStart(); + pgStart(); + scrStart(); + selStart(); + chgStart(); + subStart(); + unlStart(); } export function stop(): void { - timeline.stop(); - click.stop(); - clipboard.stop(); - pointer.stop(); - input.stop(); - resize.stop(); - visibility.stop(); - focus.stop(); - pageshow.stop(); - scroll.stop(); - selection.stop(); - change.stop(); - submit.stop(); - unload.stop() + tlStop(); + clkStop(); + cbStop(); + ptrStop(); + inpStop(); + resStop(); + visStop(); + focStop(); + pgStop(); + scrStop(); + selStop(); + chgStop(); + subStop(); + unlStop(); } export function observe(root: Node): void { - scroll.observe(root); - // Only monitor following interactions if the root node is a document - // In case of shadow DOM, following events automatically bubble up to the parent document. + scrObs(root); if (root.nodeType === Node.DOCUMENT_NODE) { - click.observe(root); - clipboard.observe(root); - pointer.observe(root); - input.observe(root); - selection.observe(root); - change.observe(root); - submit.observe(root); + clkObs(root); + cbObs(root); + ptrObs(root); + inpObs(root); + selObs(root); + chgObs(root); + subObs(root); } } diff --git a/packages/clarity-js/src/layout/index.ts b/packages/clarity-js/src/layout/index.ts index e6ea9d12..59822349 100644 --- a/packages/clarity-js/src/layout/index.ts +++ b/packages/clarity-js/src/layout/index.ts @@ -1,44 +1,39 @@ -import * as discover from "@src/layout/discover"; -import * as doc from "@src/layout/document"; -import * as dom from "@src/layout/dom"; -import * as mutation from "@src/layout/mutation"; -import * as region from "@src/layout/region"; -import * as style from "@src/layout/style"; -import * as animation from "@src/layout/animation"; -import * as custom from "@src/layout/custom"; +import { start as discStart } from "@src/layout/discover"; +import { start as docStart, stop as docStop } from "@src/layout/document"; +import { start as domStart, stop as domStop } from "@src/layout/dom"; +import { start as mutStart, stop as mutStop } from "@src/layout/mutation"; +import { start as regStart, stop as regStop } from "@src/layout/region"; +import { start as styStart, stop as styStop } from "@src/layout/style"; +import { start as animStart, stop as animStop } from "@src/layout/animation"; +import { start as custStart, stop as custStop } from "@src/layout/custom"; import { bind } from "@src/core/event"; import config from "@src/core/config"; export { hashText } from "@src/layout/dom"; export function start(): void { - // The order below is important - // and is determined by interdependencies of modules - doc.start(); - region.start(); - dom.start(); + docStart(); + regStart(); + domStart(); if (config.delayDom) { - // Lazy load layout module as part of page load time performance improvements experiment bind(window, 'load', () => { - mutation.start(); + mutStart(); }); } else { - mutation.start(); + mutStart(); } - // IMPORTANT: Start custom element detection BEFORE discover - // This ensures pre-existing custom elements are registered before DOM traversal - custom.start(); - discover.start(); - style.start(); - animation.start(); + custStart(); + discStart(); + styStart(); + animStart(); } export function stop(): void { - region.stop(); - dom.stop(); - mutation.stop(); - doc.stop(); - style.stop(); - animation.stop(); - custom.stop(); + regStop(); + domStop(); + mutStop(); + docStop(); + styStop(); + animStop(); + custStop(); } diff --git a/packages/clarity-js/src/layout/node.ts b/packages/clarity-js/src/layout/node.ts index 931afb99..f3e1668b 100644 --- a/packages/clarity-js/src/layout/node.ts +++ b/packages/clarity-js/src/layout/node.ts @@ -1,6 +1,6 @@ import { Constant, Source } from "@clarity-types/layout"; import { Code, Dimension, Severity } from "@clarity-types/data"; -import * as dom from "./dom"; +import { add as domAdd, update as domUpdate, has as domHas, iframe as domIframe, get as domGet, parse as domParse, sameorigin, iframeContent, removeIFrame } from "./dom"; import * as event from "@src/core/event"; import * as dimension from "@src/data/dimension"; import * as internal from "@src/diagnostic/internal"; @@ -14,11 +14,15 @@ import { electron } from "@src/data/metadata"; const IGNORE_ATTRIBUTES = ["title", "alt", "onload", "onfocus", "onerror", "data-drupal-form-submit-last", "aria-label"]; const newlineRegex = /[\r\n]+/g; +function domCall(isAdd: boolean, node: Node, parent: Node, data: any, source: Source): void { + isAdd ? domAdd(node, parent, data, source) : domUpdate(node, parent, data, source); +} + export default function (node: Node, source: Source, timestamp: number): Node { let child: Node = null; // Do not track this change if we are attempting to remove a node before discovering it - if (source === Source.ChildListRemove && dom.has(node) === false) { return child; } + if (source === Source.ChildListRemove && domHas(node) === false) { return child; } // Special handling for text nodes that belong to style nodes if (source !== Source.Discover && @@ -28,25 +32,24 @@ export default function (node: Node, source: Source, timestamp: number): Node { node = node.parentNode; } - let add = dom.has(node) === false; - let call = add ? "add" : "update"; + let isAdd = domHas(node) === false; let parent = node.parentElement ? node.parentElement : null; let insideFrame = node.ownerDocument !== document; switch (node.nodeType) { case Node.DOCUMENT_TYPE_NODE: - parent = insideFrame && node.parentNode ? dom.iframe(node.parentNode) : parent; + parent = insideFrame && node.parentNode ? domIframe(node.parentNode) : parent; let docTypePrefix = insideFrame ? Constant.IFramePrefix : Constant.Empty; let doctype = node as DocumentType; let docName = doctype.name ? doctype.name : Constant.HTML; let docAttributes = { name: docName, publicId: doctype.publicId, systemId: doctype.systemId }; let docData = { tag: docTypePrefix + Constant.DocumentTag, attributes: docAttributes }; - dom[call](node, parent, docData, source); + domCall(isAdd, node, parent, docData, source); break; case Node.DOCUMENT_NODE: // We check for regions in the beginning when discovering document and // later whenever there are new additions or modifications to DOM (mutations) if (node === document) { - dom.parse(document); + domParse(document); } checkDocumentStyles(node as Document, timestamp); observe(node as Document); @@ -54,9 +57,9 @@ export default function (node: Node, source: Source, timestamp: number): Node { case Node.DOCUMENT_FRAGMENT_NODE: let shadowRoot = (node as ShadowRoot); if (shadowRoot.host) { - dom.parse(shadowRoot); + domParse(shadowRoot); let type = typeof (shadowRoot.constructor); - if (type === Constant.Function && shadowRoot.constructor.toString().indexOf(Constant.NativeCode) >= 0) { + if (type === Constant.Function && shadowRoot.constructor.toString().includes(Constant.NativeCode)) { observe(shadowRoot); // See: https://wicg.github.io/construct-stylesheets/ for more details on adoptedStyleSheets. @@ -65,12 +68,12 @@ export default function (node: Node, source: Source, timestamp: number): Node { // cause any unintended side effect to the page. We will re-evaluate after we gather more real world data on this. let style = Constant.Empty as string; let fragmentData = { tag: Constant.ShadowDomTag, attributes: { style } }; - dom[call](node, shadowRoot.host, fragmentData, source); + domCall(isAdd, node, shadowRoot.host, fragmentData, source); } else { // If the browser doesn't support shadow DOM natively, we detect that, and send appropriate tag back. // The differentiation is important because we don't have to observe pollyfill shadow DOM nodes, // the same way we observe real shadow DOM nodes (encapsulation provided by the browser). - dom[call](node, shadowRoot.host, { tag: Constant.PolyfillShadowDomTag, attributes: {} }, source); + domCall(isAdd, node, shadowRoot.host, { tag: Constant.PolyfillShadowDomTag, attributes: {} }, source); } checkDocumentStyles(node as Document, timestamp); } @@ -83,9 +86,9 @@ export default function (node: Node, source: Source, timestamp: number): Node { // Also, we do not track text nodes for STYLE tags // The only exception is when we receive a mutation to remove the text node, in that case // parent will be null, but we can still process the node by checking it's an update call. - if (call === "update" || (parent && dom.has(parent) && parent.tagName !== "STYLE" && parent.tagName !== "NOSCRIPT")) { + if (!isAdd || (parent && domHas(parent) && parent.tagName !== "STYLE" && parent.tagName !== "NOSCRIPT")) { let textData = { tag: Constant.TextTag, value: node.nodeValue }; - dom[call](node, parent, textData, source); + domCall(isAdd, node, parent, textData, source); } break; case Node.ELEMENT_NODE: @@ -100,10 +103,10 @@ export default function (node: Node, source: Source, timestamp: number): Node { switch (tag) { case "HTML": - parent = insideFrame && parent ? dom.iframe(parent) : parent; + parent = insideFrame && parent ? domIframe(parent) : parent; let htmlPrefix = insideFrame ? Constant.IFramePrefix : Constant.Empty; let htmlData = { tag: htmlPrefix + tag, attributes }; - dom[call](node, parent, htmlData, source); + domCall(isAdd, node, parent, htmlData, source); break; case "SCRIPT": if (Constant.Type in attributes && attributes[Constant.Type] === Constant.JsonLD) { @@ -116,7 +119,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { // keeping the noscript tag but ignoring its contents. Some HTML markup relies on having these tags // to maintain parity with the original css view, but we don't want to execute any noscript in Clarity let noscriptData = { tag, attributes: {}, value: '' }; - dom[call](node, parent, noscriptData, source); + domCall(isAdd, node, parent, noscriptData, source); break; case "META": var key = (Constant.Property in attributes ? @@ -141,11 +144,11 @@ export default function (node: Node, source: Source, timestamp: number): Node { let head = { tag, attributes }; let l = insideFrame && node.ownerDocument?.location ? node.ownerDocument.location : location; head.attributes[Constant.Base] = l.protocol + "//" + l.host + l.pathname; - dom[call](node, parent, head, source); + domCall(isAdd, node, parent, head, source); break; case "BASE": // Override the auto detected base path to explicit value specified in this tag - let baseHead = dom.get(node.parentElement); + let baseHead = domGet(node.parentElement); if (baseHead) { // We create "a" element so we can generate protocol and hostname for relative paths like "/path/" let a = document.createElement("a"); @@ -155,12 +158,12 @@ export default function (node: Node, source: Source, timestamp: number): Node { break; case "STYLE": let styleData = { tag, attributes, value: getStyleValue(element as HTMLStyleElement) }; - dom[call](node, parent, styleData, source); + domCall(isAdd, node, parent, styleData, source); break; case "IFRAME": let iframe = node as HTMLIFrameElement; let frameData = { tag, attributes }; - if (dom.sameorigin(iframe)) { + if (sameorigin(iframe)) { mutation.monitor(iframe); frameData.attributes[Constant.SameOrigin] = "true"; if (iframe.contentDocument && iframe.contentWindow && iframe.contentDocument.readyState !== "loading") { @@ -170,7 +173,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { if (source === Source.ChildListRemove) { removeObserver(iframe); } - dom[call](node, parent, frameData, source); + domCall(isAdd, node, parent, frameData, source); break; case "LINK": // electron stylesheets reference the local file system - translating those @@ -180,7 +183,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { var currentStyleSheet = document.styleSheets[styleSheetIndex]; if (currentStyleSheet.ownerNode == element) { let syntheticStyleData = { tag: "STYLE", attributes, value: getCssRules(currentStyleSheet) }; - dom[call](node, parent, syntheticStyleData, source); + domCall(isAdd, node, parent, syntheticStyleData, source); break; } } @@ -188,7 +191,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { } // for links that aren't electron style sheets we can process them normally let linkData = { tag, attributes }; - dom[call](node, parent, linkData, source); + domCall(isAdd, node, parent, linkData, source); break; case "VIDEO": case "AUDIO": @@ -198,13 +201,13 @@ export default function (node: Node, source: Source, timestamp: number): Node { attributes[Constant.Src] = ""; } let mediaTag = { tag, attributes }; - dom[call](node, parent, mediaTag, source); + domCall(isAdd, node, parent, mediaTag, source); break; default: custom.check(element.localName); let data = { tag, attributes }; if (element.shadowRoot) { child = element.shadowRoot; } - dom[call](node, parent, data, source); + domCall(isAdd, node, parent, data, source); break; } break; @@ -215,7 +218,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { } function observe(root: Document | ShadowRoot): void { - if (dom.has(root) || event.has(root)) { return; } + if (domHas(root) || event.has(root)) { return; } mutation.observe(root); // Observe mutations for this root node interaction.observe(root); // Observe interactions for this root node } @@ -224,7 +227,7 @@ export function removeObserver(root: HTMLIFrameElement): void { // iframes will have load event listeners and they should be removed when iframe is removed // from the document event.unbind(root); - const { doc = null, win = null } = dom.iframeContent(root) || {}; + const { doc = null, win = null } = iframeContent(root) || {}; if (win) { // For iframes, scroll event is observed on content window and this needs to be removed as well @@ -238,7 +241,7 @@ export function removeObserver(root: HTMLIFrameElement): void { mutation.disconnect(doc); // Remove iframe and content document from maps tracking them - dom.removeIFrame(root, doc); + removeIFrame(root, doc); } } @@ -279,7 +282,7 @@ function getAttributes(element: HTMLElement): { [key: string]: string } { if (attributes && attributes.length > 0) { for (let i = 0; i < attributes.length; i++) { let name = attributes[i].name; - if (IGNORE_ATTRIBUTES.indexOf(name) < 0) { + if (!IGNORE_ATTRIBUTES.includes(name)) { output[name] = attributes[i].value; } } diff --git a/packages/clarity-js/src/performance/index.ts b/packages/clarity-js/src/performance/index.ts index d026ef50..1de2931a 100644 --- a/packages/clarity-js/src/performance/index.ts +++ b/packages/clarity-js/src/performance/index.ts @@ -1,12 +1,12 @@ -import * as navigation from "@src/performance/navigation"; -import * as observer from "@src/performance/observer"; +import { reset as navReset } from "@src/performance/navigation"; +import { start as obsStart, stop as obsStop } from "@src/performance/observer"; export function start(): void { - navigation.reset(); - observer.start(); + navReset(); + obsStart(); } export function stop(): void { - observer.stop(); - navigation.reset(); + obsStop(); + navReset(); } From a188d21d158558508203747960e93dc1a3f1b216 Mon Sep 17 00:00:00 2001 From: Manu Nair Date: Sun, 29 Mar 2026 16:23:02 -0700 Subject: [PATCH 3/8] refactor: deduplicate repeated code patterns and simplify modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract helpers to deduplicate repeated code: - baseline.ts: Object.assign + updatePointer helper - mutation.ts: proxyRule helper for CSS rule proxying - style.ts: proxyStyleMethod helper + simpler arraysEqual - history.ts: proxyHistory helper for pushState/replaceState - animation.ts: simplified overrideAnimationHelper - metric.ts: init() helper for count/sum dedup - compress.ts: Response API replacing manual stream reader - signal.ts: eliminate redundant parseSignals() wrapper - Shorten internal property names: clarityOverrides→__clr --- packages/clarity-js/src/core/history.ts | 36 +++------- packages/clarity-js/src/data/baseline.ts | 66 ++++------------- packages/clarity-js/src/data/compress.ts | 17 +---- packages/clarity-js/src/data/metric.ts | 9 ++- packages/clarity-js/src/data/signal.ts | 17 +---- packages/clarity-js/src/layout/animation.ts | 31 ++++---- packages/clarity-js/src/layout/custom.ts | 8 +-- packages/clarity-js/src/layout/mutation.ts | 79 +++++++-------------- packages/clarity-js/src/layout/style.ts | 57 ++++++--------- packages/clarity-js/types/global.d.ts | 2 +- 10 files changed, 97 insertions(+), 225 deletions(-) diff --git a/packages/clarity-js/src/core/history.ts b/packages/clarity-js/src/core/history.ts index 772930c0..6bd6e8a4 100644 --- a/packages/clarity-js/src/core/history.ts +++ b/packages/clarity-js/src/core/history.ts @@ -14,40 +14,26 @@ export function start(): void { url = getCurrentUrl(); count = 0; bind(window, "popstate", compute); + pushState = proxyHistory(pushState, "pushState"); + replaceState = proxyHistory(replaceState, "replaceState"); +} - // Add a proxy to history.pushState function - // Wrap in try-catch to handle Safari iOS where history methods may be readonly - if (pushState === null) { - try { - pushState = history.pushState; - history.pushState = function(): void { - pushState.apply(this, arguments); - if (core.active() && check()) { - compute(); - } - }; - } catch (e) { - // history.pushState is readonly in this environment (e.g., Safari iOS WKWebView) - pushState = null; - } - } - - // Add a proxy to history.replaceState function - // Wrap in try-catch to handle Safari iOS where history methods may be readonly - if (replaceState === null) { +function proxyHistory(original: Function, method: string): Function { + if (original === null) { try { - replaceState = history.replaceState; - history.replaceState = function(): void { - replaceState.apply(this, arguments); + original = history[method]; + history[method] = function(): void { + original.apply(this, arguments); if (core.active() && check()) { compute(); } }; } catch (e) { - // history.replaceState is readonly in this environment (e.g., Safari iOS WKWebView) - replaceState = null; + // history method may be readonly (e.g., Safari iOS WKWebView) + original = null; } } + return original; } function check(): boolean { diff --git a/packages/clarity-js/src/data/baseline.ts b/packages/clarity-js/src/data/baseline.ts index 65e2388d..0f9dbf77 100644 --- a/packages/clarity-js/src/data/baseline.ts +++ b/packages/clarity-js/src/data/baseline.ts @@ -16,34 +16,7 @@ export function reset(): void { // Baseline state holds the previous values - if it is updated in the current payload, // reset the state to current value after sending the previous state if (update) { - state = { time: time(), event: Event.Baseline, data: { - visible: buffer.visible, - docWidth: buffer.docWidth, - docHeight: buffer.docHeight, - screenWidth: buffer.screenWidth, - screenHeight: buffer.screenHeight, - scrollX: buffer.scrollX, - scrollY: buffer.scrollY, - pointerX: buffer.pointerX, - pointerY: buffer.pointerY, - activityTime: buffer.activityTime, - scrollTime: buffer.scrollTime, - pointerTime: buffer.pointerTime, - moveX: buffer.moveX, - moveY: buffer.moveY, - moveTime: buffer.moveTime, - downX: buffer.downX, - downY: buffer.downY, - downTime: buffer.downTime, - upX: buffer.upX, - upY: buffer.upY, - upTime: buffer.upTime, - pointerPrevX: buffer.pointerPrevX, - pointerPrevY: buffer.pointerPrevY, - pointerPrevTime: buffer.pointerPrevTime, - modules: buffer.modules, - } - }; + state = { time: time(), event: Event.Baseline, data: Object.assign({}, buffer) }; } buffer = buffer ? buffer : { visible: BooleanFlag.True, @@ -93,47 +66,36 @@ export function track(event: Event, x: number, y: number, time?: number): void { buffer.moveX = x; buffer.moveY = y; buffer.moveTime = time; - buffer.pointerPrevX = buffer.pointerX; - buffer.pointerPrevY = buffer.pointerY; - buffer.pointerPrevTime = buffer.pointerTime; - buffer.pointerX = x; - buffer.pointerY = y; - buffer.pointerTime = time; + updatePointer(x, y, time); break; case Event.MouseDown: buffer.downX = x; buffer.downY = y; buffer.downTime = time; - buffer.pointerPrevX = buffer.pointerX; - buffer.pointerPrevY = buffer.pointerY; - buffer.pointerPrevTime = buffer.pointerTime; - buffer.pointerX = x; - buffer.pointerY = y; - buffer.pointerTime = time; + updatePointer(x, y, time); break; case Event.MouseUp: buffer.upX = x; buffer.upY = y; buffer.upTime = time; - buffer.pointerPrevX = buffer.pointerX; - buffer.pointerPrevY = buffer.pointerY; - buffer.pointerPrevTime = buffer.pointerTime; - buffer.pointerX = x; - buffer.pointerY = y; - buffer.pointerTime = time; + updatePointer(x, y, time); break; default: - buffer.pointerPrevX = buffer.pointerX; - buffer.pointerPrevY = buffer.pointerY; - buffer.pointerPrevTime = buffer.pointerTime; - buffer.pointerX = x; - buffer.pointerY = y; - buffer.pointerTime = time; + updatePointer(x, y, time); break; } update = true; } +function updatePointer(x: number, y: number, time: number): void { + buffer.pointerPrevX = buffer.pointerX; + buffer.pointerPrevY = buffer.pointerY; + buffer.pointerPrevTime = buffer.pointerTime; + buffer.pointerX = x; + buffer.pointerY = y; + buffer.pointerTime = time; +} + export function activity(t: number): void { buffer.activityTime = t; } diff --git a/packages/clarity-js/src/data/compress.ts b/packages/clarity-js/src/data/compress.ts index 6671fbb0..08d122d5 100644 --- a/packages/clarity-js/src/data/compress.ts +++ b/packages/clarity-js/src/data/compress.ts @@ -5,27 +5,12 @@ const supported = Constant.CompressionStream in window; export default async function(input: string): Promise { try { if (supported) { - // Create a readable stream from given input string and - // pipe it through text encoder and compression stream to gzip it const stream = new ReadableStream({async start(controller) { controller.enqueue(input); controller.close(); }}).pipeThrough(new TextEncoderStream()).pipeThrough(new window[Constant.CompressionStream]("gzip")); - return new Uint8Array(await read(stream)); + return new Uint8Array(await new Response(stream).arrayBuffer()); } } catch { /* do nothing */ } return null; } - -async function read(stream: ReadableStream): Promise { - const reader = stream.getReader(); - const chunks:number[] = []; - let done = false; - let value: number[] = []; - while (!done) { - ({ done, value } = await reader.read()); - if (done) { return chunks; } - chunks.push(...value); - } - return chunks; -} diff --git a/packages/clarity-js/src/data/metric.ts b/packages/clarity-js/src/data/metric.ts index f86e7611..a0923a2c 100644 --- a/packages/clarity-js/src/data/metric.ts +++ b/packages/clarity-js/src/data/metric.ts @@ -15,17 +15,20 @@ export function stop(): void { updates = {}; } -export function count(metric: Metric): void { +function init(metric: Metric): void { if (!(metric in data)) { data[metric] = 0; } if (!(metric in updates)) { updates[metric] = 0; } +} + +export function count(metric: Metric): void { + init(metric); data[metric]++; updates[metric]++; } export function sum(metric: Metric, value: number): void { if (value !== null) { - if (!(metric in data)) { data[metric] = 0; } - if (!(metric in updates)) { updates[metric] = 0; } + init(metric); data[metric] += value; updates[metric] += value; } diff --git a/packages/clarity-js/src/data/signal.ts b/packages/clarity-js/src/data/signal.ts index f3d78754..f229924b 100644 --- a/packages/clarity-js/src/data/signal.ts +++ b/packages/clarity-js/src/data/signal.ts @@ -6,24 +6,11 @@ export function signal(cb: SignalCallback): void { signalCallback = cb; } -function parseSignals(signalsPayload: string): ClaritySignal[] { - try{ - const parsedSignals: ClaritySignal[] = JSON.parse(signalsPayload); - return parsedSignals; - }catch{ - return [] - } -} - export function signalsEvent(signalsPayload: string) { try { - if (!signalCallback) { - return; + if (signalCallback) { + (JSON.parse(signalsPayload) as ClaritySignal[]).forEach(s => signalCallback(s)); } - const signals = parseSignals(signalsPayload); - signals.forEach((signal) => { - signalCallback(signal); - }); } catch { //do nothing } diff --git a/packages/clarity-js/src/layout/animation.ts b/packages/clarity-js/src/layout/animation.ts index 6e022f91..83e77fbc 100644 --- a/packages/clarity-js/src/layout/animation.ts +++ b/packages/clarity-js/src/layout/animation.ts @@ -8,13 +8,9 @@ import * as core from "@src/core"; export let state: AnimationState[] = []; let elementAnimate: (keyframes: Keyframe[] | PropertyIndexedKeyframes, options?: number | KeyframeAnimationOptions) => Animation = null; -let animationPlay: () => void = null; -let animationPause: () => void = null; -let animationCommitStyles: () => void = null; -let animationCancel: () => void = null; -let animationFinish: () => void = null; -const animationId = 'clarityAnimationId'; -const operationCount = 'clarityOperationCount'; +let overridden = false; +const animationId = '__clrAId'; +const operationCount = '__clrOCnt'; const maxOperations = 20; export function start(): void { @@ -27,11 +23,10 @@ export function start(): void { window["KeyframeEffect"].prototype.getTiming ) { reset(); - overrideAnimationHelper(animationPlay, "play"); - overrideAnimationHelper(animationPause, "pause"); - overrideAnimationHelper(animationCommitStyles, "commitStyles"); - overrideAnimationHelper(animationCancel, "cancel"); - overrideAnimationHelper(animationFinish, "finish"); + if (!overridden) { + overridden = true; + ["play", "pause", "commitStyles", "cancel", "finish"].forEach(overrideAnimationHelper); + } if (elementAnimate === null) { elementAnimate = Element.prototype.animate; Element.prototype.animate = function(): Animation { @@ -81,15 +76,13 @@ export function stop(): void { reset(); } -function overrideAnimationHelper(functionToOverride: () => void, name: string) { - if (functionToOverride === null) { - functionToOverride = Animation.prototype[name]; - Animation.prototype[name] = function(): void { +function overrideAnimationHelper(name: string) { + let original = Animation.prototype[name]; + Animation.prototype[name] = function(): void { trackAnimationOperation(this, name); - return functionToOverride.apply(this, arguments); - } + return original.apply(this, arguments); } - } +} function trackAnimationOperation(animation: Animation, name: string) { if (core.active()) { diff --git a/packages/clarity-js/src/layout/custom.ts b/packages/clarity-js/src/layout/custom.ts index 53a933d2..1890dc3d 100644 --- a/packages/clarity-js/src/layout/custom.ts +++ b/packages/clarity-js/src/layout/custom.ts @@ -23,14 +23,14 @@ export function check(tag: string) { export function start() { // Wrap in try-catch to handle Safari iOS where window properties or customElements.define may be readonly try { - window.clarityOverrides = window.clarityOverrides || {}; - if (window.customElements?.define && !window.clarityOverrides.define) { - window.clarityOverrides.define = window.customElements.define; + window.__clr = window.__clr || {}; + if (window.customElements?.define && !window.__clr.define) { + window.__clr.define = window.customElements.define; window.customElements.define = function () { if (active()) { register(arguments[0]); } - return window.clarityOverrides.define.apply(this, arguments); + return window.__clr.define.apply(this, arguments); }; } } catch (e) { diff --git a/packages/clarity-js/src/layout/mutation.ts b/packages/clarity-js/src/layout/mutation.ts index ee4d753e..898ae4b5 100644 --- a/packages/clarity-js/src/layout/mutation.ts +++ b/packages/clarity-js/src/layout/mutation.ts @@ -138,7 +138,7 @@ async function processMutation(timer: Timer, mutation: MutationRecord, instance: switch (type) { case Constant.Attributes: - if (IGNORED_ATTRIBUTES.indexOf(mutation.attributeName) < 0) { + if (!IGNORED_ATTRIBUTES.includes(mutation.attributeName)) { processNode(target, Source.Attributes, timestamp); } break; @@ -292,7 +292,7 @@ function processThrottledMutations(): void { export function schedule(node: Node): Node { // Only schedule manual trigger for this node if it's not already in the queue - if (queue.indexOf(node) < 0) { + if (!queue.includes(node)) { queue.push(node); } @@ -346,67 +346,40 @@ function proxyStyleRules(win: any): void { return; } - win.clarityOverrides = win.clarityOverrides || {}; + win.__clr = win.__clr || {}; - // Some popular open source libraries, like styled-components, optimize performance - // by injecting CSS using insertRule API vs. appending text node. A side effect of - // using javascript API is that it doesn't trigger DOM mutation and therefore we - // need to override the insertRule API and listen for changes manually. - if ("CSSStyleSheet" in win && win.CSSStyleSheet && win.CSSStyleSheet.prototype && win.clarityOverrides.InsertRule === undefined) { - win.clarityOverrides.InsertRule = win.CSSStyleSheet.prototype.insertRule; - win.CSSStyleSheet.prototype.insertRule = function (): number { - if (core.active()) { - schedule(this.ownerNode); - } - return win.clarityOverrides.InsertRule.apply(this, arguments); - }; - } - - if ("CSSMediaRule" in win && win.CSSMediaRule && win.CSSMediaRule.prototype && win.clarityOverrides.MediaInsertRule === undefined) { - win.clarityOverrides.MediaInsertRule = win.CSSMediaRule.prototype.insertRule; - win.CSSMediaRule.prototype.insertRule = function (): number { - if (core.active()) { - schedule(this.parentStyleSheet.ownerNode); - } - return win.clarityOverrides.MediaInsertRule.apply(this, arguments); - }; - } - - if ("CSSStyleSheet" in win && win.CSSStyleSheet && win.CSSStyleSheet.prototype && win.clarityOverrides.DeleteRule === undefined) { - win.clarityOverrides.DeleteRule = win.CSSStyleSheet.prototype.deleteRule; - win.CSSStyleSheet.prototype.deleteRule = function (): void { - if (core.active()) { - schedule(this.ownerNode); - } - return win.clarityOverrides.DeleteRule.apply(this, arguments); - }; - } - - if ("CSSMediaRule" in win && win.CSSMediaRule && win.CSSMediaRule.prototype && win.clarityOverrides.MediaDeleteRule === undefined) { - win.clarityOverrides.MediaDeleteRule = win.CSSMediaRule.prototype.deleteRule; - win.CSSMediaRule.prototype.deleteRule = function (): void { - if (core.active()) { - schedule(this.parentStyleSheet.ownerNode); - } - return win.clarityOverrides.MediaDeleteRule.apply(this, arguments); - }; - } + // Proxy insertRule/deleteRule on CSSStyleSheet and CSSMediaRule to detect dynamic style changes. + // Libraries like styled-components use insertRule API instead of DOM text nodes. + proxyRule(win, "CSSStyleSheet", "InsertRule", "insertRule", function() { return this.ownerNode; }); + proxyRule(win, "CSSStyleSheet", "DeleteRule", "deleteRule", function() { return this.ownerNode; }); + proxyRule(win, "CSSMediaRule", "MediaInsertRule", "insertRule", function() { return this.parentStyleSheet.ownerNode; }); + proxyRule(win, "CSSMediaRule", "MediaDeleteRule", "deleteRule", function() { return this.parentStyleSheet.ownerNode; }); // Add a hook to attachShadow API calls - // In case we are unable to add a hook and browser throws an exception, - // reset attachShadow variable and resume processing like before - if ("Element" in win && win.Element && win.Element.prototype && win.clarityOverrides.AttachShadow === undefined) { - win.clarityOverrides.AttachShadow = win.Element.prototype.attachShadow; + if ("Element" in win && win.Element && win.Element.prototype && win.__clr.AttachShadow === undefined) { + win.__clr.AttachShadow = win.Element.prototype.attachShadow; try { win.Element.prototype.attachShadow = function (): ShadowRoot { if (core.active()) { - return schedule(win.clarityOverrides.AttachShadow.apply(this, arguments)) as ShadowRoot; + return schedule(win.__clr.AttachShadow.apply(this, arguments)) as ShadowRoot; } else { - return win.clarityOverrides.AttachShadow.apply(this, arguments); + return win.__clr.AttachShadow.apply(this, arguments); } }; } catch { - win.clarityOverrides.AttachShadow = null; + win.__clr.AttachShadow = null; } } +} + +function proxyRule(win: any, cls: string, key: string, method: string, getNode: () => Node): void { + if (cls in win && win[cls] && win[cls].prototype && win.__clr[key] === undefined) { + win.__clr[key] = win[cls].prototype[method]; + win[cls].prototype[method] = function (): any { + if (core.active()) { + schedule(getNode.call(this)); + } + return win.__clr[key].apply(this, arguments); + }; + } } \ No newline at end of file diff --git a/packages/clarity-js/src/layout/style.ts b/packages/clarity-js/src/layout/style.ts index 7b08991f..88594242 100644 --- a/packages/clarity-js/src/layout/style.ts +++ b/packages/clarity-js/src/layout/style.ts @@ -10,7 +10,7 @@ import { getCssRules } from "./node"; export let sheetUpdateState: StyleSheetState[] = []; export let sheetAdoptionState: StyleSheetState[] = []; -const styleSheetId = 'claritySheetId'; +const styleSheetId = '__clrSId'; let styleSheetMap = {}; let styleTimeMap: {[key: string]: number} = {}; let documentNodes = []; @@ -21,39 +21,26 @@ function proxyStyleRules(win: any) { return; } - win.clarityOverrides = win.clarityOverrides || {}; + win.__clr = win.__clr || {}; if (win['CSSStyleSheet'] && win.CSSStyleSheet.prototype) { - if (win.clarityOverrides.replace === undefined) { - win.clarityOverrides.replace = win.CSSStyleSheet.prototype.replace; - win.CSSStyleSheet.prototype.replace = function(): Promise { - if (core.active()) { - // if we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it - // and attached the sheet to a document. This way the timestamp of the style sheet creation will align - // to when it is used in the document rather than potentially being misaligned during the traverse process. - if (createdSheetIds.indexOf(this[styleSheetId]) > -1) { - trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Replace, arguments[0]); - } - } - return win.clarityOverrides.replace.apply(this, arguments); - }; - } + proxyStyleMethod(win, "replace", StyleSheetOperation.Replace); + proxyStyleMethod(win, "replaceSync", StyleSheetOperation.ReplaceSync); + } +} - if (win.clarityOverrides.replaceSync === undefined) { - win.clarityOverrides.replaceSync = win.CSSStyleSheet.prototype.replaceSync; - win.CSSStyleSheet.prototype.replaceSync = function(): void { - if (core.active()) { - // if we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it - // and attached the sheet to a document. This way the timestamp of the style sheet creation will align - // to when it is used in the document rather than potentially being misaligned during the traverse process. - if (createdSheetIds.indexOf(this[styleSheetId]) > -1) { - trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.ReplaceSync, arguments[0]); - } +function proxyStyleMethod(win: any, method: string, operation: StyleSheetOperation): void { + if (win.__clr[method] === undefined) { + win.__clr[method] = win.CSSStyleSheet.prototype[method]; + win.CSSStyleSheet.prototype[method] = function(): any { + if (core.active()) { + if (createdSheetIds.includes(this[styleSheetId])) { + trackStyleChange(time(), this[styleSheetId], operation, arguments[0]); } - return win.clarityOverrides.replaceSync.apply(this, arguments); - }; - } - } + } + return win.__clr[method].apply(this, arguments); + }; + } } export function start(): void { @@ -63,7 +50,7 @@ export function start(): void { export function checkDocumentStyles(documentNode: Document, timestamp: number): void { if (config.lean && config.lite) { return; } - if (documentNodes.indexOf(documentNode) === -1) { + if (!documentNodes.includes(documentNode)) { documentNodes.push(documentNode); if (documentNode.defaultView) { proxyStyleRules(documentNode.defaultView); @@ -80,7 +67,7 @@ export function checkDocumentStyles(documentNode: Document, timestamp: number): // For SPA or times in which Clarity restarts on a given page, our visualizer would lose context // on the previously created style sheet for page N-1. // Then we synthetically call replaceSync with its contents to bootstrap it - if (!styleSheet[styleSheetId] || createdSheetIds.indexOf(styleSheet[styleSheetId]) === -1) { + if (!styleSheet[styleSheetId] || !createdSheetIds.includes(styleSheet[styleSheetId])) { styleSheet[styleSheetId] = shortid(); createdSheetIds.push(styleSheet[styleSheetId]); trackStyleChange(timestamp, styleSheet[styleSheetId], StyleSheetOperation.Create); @@ -152,9 +139,5 @@ function trackStyleAdoption(time: number, id: number, operation: StyleSheetOpera } function arraysEqual(a: string[], b: string[]): boolean { - if (a.length !== b.length) { - return false; - } - - return a.every((value, index) => value === b[index]); + return a.length === b.length && a.every((v, i) => v === b[i]); } \ No newline at end of file diff --git a/packages/clarity-js/types/global.d.ts b/packages/clarity-js/types/global.d.ts index 803f2483..0ddf4940 100644 --- a/packages/clarity-js/types/global.d.ts +++ b/packages/clarity-js/types/global.d.ts @@ -1,6 +1,6 @@ declare global { interface Window { - clarityOverrides?: { [key: string]: ((...args: any[]) => any) | undefined }; + __clr?: { [key: string]: ((...args: any[]) => any) | undefined }; google_tag_data?: { ics: { addListener: (keys: string[], callback: () => void) => void; From 0ff538acf04004ce3f6f9886af06c049820b6c5e Mon Sep 17 00:00:00 2001 From: Manu Nair Date: Sun, 29 Mar 2026 16:23:02 -0700 Subject: [PATCH 4/8] refactor: modernize syntax for smaller minified output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modernize syntax patterns across 26 source files for smaller minified output: - indexOf(x)>=0 → includes(x) across ~27 occurrences - Template literals → string concatenation (avoids ES5 .concat()) - Sequential tokens.push(a); push(b) → push(a, b) - .toString() → ""+x string coercion - Inline single-use str() helper in layout/encode.ts - Local alias for b.data in data/encode.ts --- packages/clarity-js/src/core/dynamic.ts | 2 +- packages/clarity-js/src/core/measure.ts | 2 +- packages/clarity-js/src/core/report.ts | 2 +- packages/clarity-js/src/core/scrub.ts | 6 +- packages/clarity-js/src/core/task.ts | 2 +- packages/clarity-js/src/data/consent.ts | 2 +- packages/clarity-js/src/data/cookie.ts | 10 ++-- packages/clarity-js/src/data/dimension.ts | 4 +- packages/clarity-js/src/data/encode.ts | 47 +++++---------- packages/clarity-js/src/data/metadata.ts | 10 ++-- packages/clarity-js/src/data/upload.ts | 8 +-- packages/clarity-js/src/data/variable.ts | 2 +- packages/clarity-js/src/diagnostic/encode.ts | 16 +---- packages/clarity-js/src/diagnostic/fraud.ts | 2 +- .../clarity-js/src/diagnostic/internal.ts | 4 +- packages/clarity-js/src/insight/encode.ts | 4 +- packages/clarity-js/src/insight/snapshot.ts | 2 +- packages/clarity-js/src/interaction/change.ts | 2 +- packages/clarity-js/src/interaction/click.ts | 8 +-- packages/clarity-js/src/interaction/encode.ts | 59 ++++++------------- packages/clarity-js/src/layout/dom.ts | 16 ++--- packages/clarity-js/src/layout/encode.ts | 37 ++++-------- packages/clarity-js/src/layout/schema.ts | 2 +- packages/clarity-js/src/layout/selector.ts | 20 +++---- packages/clarity-js/src/performance/encode.ts | 26 ++++---- .../clarity-js/src/performance/observer.ts | 4 +- 26 files changed, 117 insertions(+), 182 deletions(-) diff --git a/packages/clarity-js/src/core/dynamic.ts b/packages/clarity-js/src/core/dynamic.ts index 2a26cc12..6690a2e6 100644 --- a/packages/clarity-js/src/core/dynamic.ts +++ b/packages/clarity-js/src/core/dynamic.ts @@ -59,7 +59,7 @@ function load(url: string, mid: number | null): void { } }; script.onerror = () => { - report(new Error(`${DataConstant.Module}: ${url}`)); + report(new Error(DataConstant.Module + ": " + url)); }; document.head.appendChild(script); } catch (error) { diff --git a/packages/clarity-js/src/core/measure.ts b/packages/clarity-js/src/core/measure.ts index 0500044b..a047ea2f 100644 --- a/packages/clarity-js/src/core/measure.ts +++ b/packages/clarity-js/src/core/measure.ts @@ -13,7 +13,7 @@ export default function (method: Function): Function { if (duration > Setting.LongTask) { metric.count(Metric.LongTaskCount); metric.max(Metric.ThreadBlockedTime, duration); - method.dn && internal.log(Code.FunctionExecutionTime, Severity.Info, `${method.dn}-${duration}`); + method.dn && internal.log(Code.FunctionExecutionTime, Severity.Info, method.dn + "-" + duration); } }; } diff --git a/packages/clarity-js/src/core/report.ts b/packages/clarity-js/src/core/report.ts index 0b8ab1d7..44a0342b 100644 --- a/packages/clarity-js/src/core/report.ts +++ b/packages/clarity-js/src/core/report.ts @@ -10,7 +10,7 @@ export function reset(): void { export function report(e: Error): Error { // Do not report the same message twice for the same page - if (history && history.indexOf(e.message) === -1) { + if (history && !history.includes(e.message)) { const url = config.report; if (url && url.length > 0 && data) { let payload: Report = {v: data.version, p: data.projectId, u: data.userId, s: data.sessionId, n: data.pageNum}; diff --git a/packages/clarity-js/src/core/scrub.ts b/packages/clarity-js/src/core/scrub.ts index 86dc145f..dfe57d9c 100644 --- a/packages/clarity-js/src/core/scrub.ts +++ b/packages/clarity-js/src/core/scrub.ts @@ -97,13 +97,13 @@ export function url(input: string, electron: boolean = false, truncate: boolean let result = input; // Replace the URL for Electron apps so we don't send back file:/// URL if (electron) { - result = `${Data.Constant.HTTPS}${Data.Constant.Electron}`; + result = Data.Constant.HTTPS + Data.Constant.Electron; } else { let drop = config.drop; if (drop && drop.length > 0 && input && input.indexOf("?") > 0) { let [path, query] = input.split("?"); let swap = Data.Constant.Dropped; - result = path + "?" + query.split("&").map(p => drop.some(x => p.indexOf(`${x}=`) === 0) ? `${p.split("=")[0]}=${swap}` : p).join("&"); + result = path + "?" + query.split("&").map(p => drop.some(x => p.indexOf(x + "=") === 0) ? (p.split("=")[0] + "=" + swap) : p).join("&"); } } @@ -120,7 +120,7 @@ function mangleText(value: string): string { let index = value.indexOf(first); let prefix = value.substr(0, index); let suffix = value.substr(index + trimmed.length); - return `${prefix}${trimmed.length.toString(36)}${suffix}`; + return prefix + trimmed.length.toString(36) + suffix; } return value; } diff --git a/packages/clarity-js/src/core/task.ts b/packages/clarity-js/src/core/task.ts index c5b462f8..83802ca5 100644 --- a/packages/clarity-js/src/core/task.ts +++ b/packages/clarity-js/src/core/task.ts @@ -136,7 +136,7 @@ export async function suspend(timer: Timer): Promise { } function key(timer: Timer): string { - return `${timer.id}.${timer.cost}`; + return timer.id + "." + timer.cost; } async function wait(): Promise { diff --git a/packages/clarity-js/src/data/consent.ts b/packages/clarity-js/src/data/consent.ts index 760ae4eb..8cdcd078 100644 --- a/packages/clarity-js/src/data/consent.ts +++ b/packages/clarity-js/src/data/consent.ts @@ -61,7 +61,7 @@ export function consent(): void { } function trackConsent(consent: ConsentType): void { - dimension.log(Dimension.Consent, consent.toString()); + dimension.log(Dimension.Consent, "" + consent); } export function trackConsentv2(consent: ConsentData): void { diff --git a/packages/clarity-js/src/data/cookie.ts b/packages/clarity-js/src/data/cookie.ts index fe74ba94..7fecc143 100644 --- a/packages/clarity-js/src/data/cookie.ts +++ b/packages/clarity-js/src/data/cookie.ts @@ -34,7 +34,7 @@ export function getCookie(key: string, limit = false): string { // If we are limiting cookies, check if the cookie value is limited if (limit) { - return decodedValue.endsWith(`${Constant.Tilde}1`) ? decodedValue.substring(0, decodedValue.length - 2) : null; + return decodedValue.endsWith(Constant.Tilde + "1") ? decodedValue.substring(0, decodedValue.length - 2) : null; } return decodedValue; @@ -56,19 +56,19 @@ export function setCookie(key: string, value: string, time: number): void { let expiry = new Date(); expiry.setDate(expiry.getDate() + time); let expires = expiry ? Constant.Expires + expiry.toUTCString() : Constant.Empty; - let cookie = `${key}=${encodedValue}${Constant.Semicolon}${expires}${Constant.Path}`; + let cookie = key + "=" + encodedValue + Constant.Semicolon + expires + Constant.Path; try { // Attempt to get the root domain only once and fall back to writing cookie on the current domain. if (rootDomain === null) { let hostname = location.hostname ? location.hostname.split(Constant.Dot) : []; // Walk backwards on a domain and attempt to set a cookie, until successful for (let i = hostname.length - 1; i >= 0; i--) { - rootDomain = `.${hostname[i]}${rootDomain ? rootDomain : Constant.Empty}`; + rootDomain = "." + hostname[i] + (rootDomain ? rootDomain : Constant.Empty); // We do not wish to attempt writing a cookie on the absolute last part of the domain, e.g. .com or .net. // So we start attempting after second-last part, e.g. .domain.com (PASS) or .co.uk (FAIL) if (i < hostname.length - 1) { // Write the cookie on the current computed top level domain - document.cookie = `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}`; + document.cookie = cookie + Constant.Semicolon + Constant.Domain + rootDomain; // Once written, check if the cookie exists and its value matches exactly with what we intended to set // Checking for exact value match helps us eliminate a corner case where the cookie may already be present with a different value // If the check is successful, no more action is required and we can return from the function since rootDomain cookie is already set @@ -85,6 +85,6 @@ export function setCookie(key: string, value: string, time: number): void { } catch { rootDomain = Constant.Empty; } - document.cookie = rootDomain ? `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}` : cookie; + document.cookie = rootDomain ? cookie + Constant.Semicolon + Constant.Domain + rootDomain : cookie; } } diff --git a/packages/clarity-js/src/data/dimension.ts b/packages/clarity-js/src/data/dimension.ts index d7283b69..77917e5e 100644 --- a/packages/clarity-js/src/data/dimension.ts +++ b/packages/clarity-js/src/data/dimension.ts @@ -22,9 +22,9 @@ export function log(dimension: Dimension, value: string): void { // Check valid value before moving ahead if (value) { // Ensure received value is casted into a string if it wasn't a string to begin with - value = `${value}`; + value = "" + value; if (!(dimension in data)) { data[dimension] = []; } - if (data[dimension].indexOf(value) < 0) { + if (!data[dimension].includes(value)) { // Limit check to ensure we have a cap on number of dimensions we can collect if (data[dimension].length > Setting.CollectionLimit) { if (!limited) { diff --git a/packages/clarity-js/src/data/encode.ts b/packages/clarity-js/src/data/encode.ts index d94dac2b..0a805c94 100644 --- a/packages/clarity-js/src/data/encode.ts +++ b/packages/clarity-js/src/data/encode.ts @@ -20,32 +20,21 @@ export default function (event: Event): void { case Event.Baseline: { const b = baseline.state; if (b && b.data) { + let d = b.data; tokens = [b.time, b.event]; - tokens.push(b.data.visible); - tokens.push(b.data.docWidth); - tokens.push(b.data.docHeight); - tokens.push(b.data.screenWidth); - tokens.push(b.data.screenHeight); - tokens.push(b.data.scrollX); - tokens.push(b.data.scrollY); - tokens.push(b.data.pointerX); - tokens.push(b.data.pointerY); - tokens.push(b.data.activityTime); - tokens.push(b.data.scrollTime); - tokens.push(b.data.pointerTime); - tokens.push(b.data.moveX); - tokens.push(b.data.moveY); - tokens.push(b.data.moveTime); - tokens.push(b.data.downX); - tokens.push(b.data.downY); - tokens.push(b.data.downTime); - tokens.push(b.data.upX); - tokens.push(b.data.upY); - tokens.push(b.data.upTime); - tokens.push(b.data.pointerPrevX); - tokens.push(b.data.pointerPrevY); - tokens.push(b.data.pointerPrevTime); - tokens.push(b.data.modules); + tokens.push( + d.visible, d.docWidth, d.docHeight, + d.screenWidth, d.screenHeight, + d.scrollX, d.scrollY, + d.pointerX, d.pointerY, + d.activityTime, d.scrollTime, + d.pointerTime, + d.moveX, d.moveY, d.moveTime, + d.downX, d.downY, d.downTime, + d.upX, d.upY, d.upTime, + d.pointerPrevX, d.pointerPrevY, d.pointerPrevTime, + d.modules + ); queue(tokens, false); } baseline.reset(); @@ -64,9 +53,7 @@ export default function (event: Event): void { queue(tokens); break; case Event.Upload: - tokens.push(track.sequence); - tokens.push(track.attempts); - tokens.push(track.status); + tokens.push(track.sequence, track.attempts, track.status); queue(tokens, false); break; case Event.Custom: @@ -146,9 +133,7 @@ export default function (event: Event): void { break; } case Event.Consent: - tokens.push(consent.data.source); - tokens.push(consent.data.ad_Storage); - tokens.push(consent.data.analytics_Storage); + tokens.push(consent.data.source, consent.data.ad_Storage, consent.data.analytics_Storage); queue(tokens, false); break; } diff --git a/packages/clarity-js/src/data/metadata.ts b/packages/clarity-js/src/data/metadata.ts index 197b0850..7fff000c 100644 --- a/packages/clarity-js/src/data/metadata.ts +++ b/packages/clarity-js/src/data/metadata.ts @@ -22,7 +22,7 @@ let defaultStatus: ConsentState = { source: ConsentSource.Default, ad_Storage: C export function start(): void { const ua = navigator && "userAgent" in navigator ? navigator.userAgent : Constant.Empty; const timezone = (typeof Intl !== 'undefined' && Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone) ?? ''; - const timezoneOffset = new Date().getTimezoneOffset().toString(); + const timezoneOffset = "" + new Date().getTimezoneOffset(); const ancestorOrigins = window.location.ancestorOrigins ? Array.from(window.location.ancestorOrigins).toString() : ''; const title = document && document.title ? document.title : Constant.Empty; electron = ua.indexOf(Constant.Electron) > 0 ? BooleanFlag.True : BooleanFlag.False; @@ -45,9 +45,9 @@ export function start(): void { dimension.log(Dimension.TabId, tab()); dimension.log(Dimension.PageLanguage, document.documentElement.lang); dimension.log(Dimension.DocumentDirection, document.dir); - dimension.log(Dimension.DevicePixelRatio, `${window.devicePixelRatio}`); - dimension.log(Dimension.Dob, u.dob.toString()); - dimension.log(Dimension.CookieVersion, u.version.toString()); + dimension.log(Dimension.DevicePixelRatio, "" + window.devicePixelRatio); + dimension.log(Dimension.Dob, "" + u.dob); + dimension.log(Dimension.CookieVersion, "" + u.version); dimension.log(Dimension.AncestorOrigins, ancestorOrigins); dimension.log(Dimension.Timezone, timezone); dimension.log(Dimension.TimezoneOffset, timezoneOffset); @@ -305,7 +305,7 @@ function session(): Session { output.session = parts[0]; output.count = num(parts[2]) + 1; output.upgrade = num(parts[3]); - output.upload = parts.length >= 6 ? `${Constant.HTTPS}${parts[5]}/${parts[4]}` : `${Constant.HTTPS}${parts[4]}`; + output.upload = parts.length >= 6 ? Constant.HTTPS + parts[5] + "/" + parts[4] : Constant.HTTPS + parts[4]; } } return output; diff --git a/packages/clarity-js/src/data/upload.ts b/packages/clarity-js/src/data/upload.ts index c7657473..aa16a687 100644 --- a/packages/clarity-js/src/data/upload.ts +++ b/packages/clarity-js/src/data/upload.ts @@ -149,9 +149,9 @@ async function upload(final: boolean = false): Promise { if(!envelope.data) return; let e = JSON.stringify(envelope.envelope(last)); - let a = `[${analysis.join()}]`; + let a = "[" + analysis.join() + "]"; - let p = sendPlaybackBytes ? `[${playback.join()}]` : Constant.Empty; + let p = sendPlaybackBytes ? "[" + playback.join() + "]" : Constant.Empty; // For final (beacon) payloads, If size is too large, we need to remove playback data if (last && p.length > 0 && (e.length + a.length + p.length > Setting.MaxBeaconPayloadBytes)) { @@ -179,7 +179,7 @@ async function upload(final: boolean = false): Promise { } function stringify(encoded: EncodedPayload): string { - return encoded.p.length > 0 ? `{"e":${encoded.e},"a":${encoded.a},"p":${encoded.p}}` : `{"e":${encoded.e},"a":${encoded.a}}`; + return encoded.p.length > 0 ? '{"e":' + encoded.e + ',"a":' + encoded.a + ',"p":' + encoded.p + '}' : '{"e":' + encoded.e + ',"a":' + encoded.a + '}'; } function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boolean = false): void { @@ -216,7 +216,7 @@ function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boo let xhr = new XMLHttpRequest(); xhr.open("POST", url, true); xhr.timeout = Setting.UploadTimeout; - xhr.ontimeout = () => { report(new Error(`${Constant.Timeout} : ${url}`)) }; + xhr.ontimeout = () => { report(new Error(Constant.Timeout + " : " + url)) }; if (sequence !== null) { xhr.onreadystatechange = (): void => { measure(check)(xhr, sequence); }; } xhr.withCredentials = true; if (zipped) { diff --git a/packages/clarity-js/src/data/variable.ts b/packages/clarity-js/src/data/variable.ts index c494fcfd..3f57103c 100644 --- a/packages/clarity-js/src/data/variable.ts +++ b/packages/clarity-js/src/data/variable.ts @@ -66,7 +66,7 @@ export function stop(): void { function redact(input: string): string { return input && input.length >= Setting.WordLength ? - `${input.substring(0,2)}${scrub(input.substring(2), Constant.Asterix, Constant.Asterix)}` : scrub(input, Constant.Asterix, Constant.Asterix); + input.substring(0,2) + scrub(input.substring(2), Constant.Asterix, Constant.Asterix) : scrub(input, Constant.Asterix, Constant.Asterix); } async function sha256(input: string): Promise { diff --git a/packages/clarity-js/src/diagnostic/encode.ts b/packages/clarity-js/src/diagnostic/encode.ts index 46e901ee..f2b9f4d4 100644 --- a/packages/clarity-js/src/diagnostic/encode.ts +++ b/packages/clarity-js/src/diagnostic/encode.ts @@ -11,28 +11,18 @@ export default async function (type: Event): Promise { switch (type) { case Event.ScriptError: - tokens.push(script.data.message); - tokens.push(script.data.line); - tokens.push(script.data.column); - tokens.push(script.data.stack); - tokens.push(scrub.url(script.data.source)); + tokens.push(script.data.message, script.data.line, script.data.column, script.data.stack, scrub.url(script.data.source)); queue(tokens); break; case Event.Log: if (internal.data) { - tokens.push(internal.data.code); - tokens.push(internal.data.name); - tokens.push(internal.data.message); - tokens.push(internal.data.stack); - tokens.push(internal.data.severity); + tokens.push(internal.data.code, internal.data.name, internal.data.message, internal.data.stack, internal.data.severity); queue(tokens, false); } break; case Event.Fraud: if (fraud.data) { - tokens.push(fraud.data.id); - tokens.push(fraud.data.target); - tokens.push(fraud.data.checksum); + tokens.push(fraud.data.id, fraud.data.target, fraud.data.checksum); queue(tokens, false); } break; diff --git a/packages/clarity-js/src/diagnostic/fraud.ts b/packages/clarity-js/src/diagnostic/fraud.ts index 62b399f1..2a60418d 100644 --- a/packages/clarity-js/src/diagnostic/fraud.ts +++ b/packages/clarity-js/src/diagnostic/fraud.ts @@ -25,7 +25,7 @@ export function check(id: number, target: number, input: string): void { if (config.fraud && id !== null && input && input.length >= Setting.WordLength) { data = { id, target, checksum: hash(input, Setting.ChecksumPrecision) }; // Only encode this event if we haven't already reported this hash - if (history.indexOf(data.checksum) < 0) { + if (!history.includes(data.checksum)) { history.push(data.checksum); encode(Event.Fraud); } diff --git a/packages/clarity-js/src/diagnostic/internal.ts b/packages/clarity-js/src/diagnostic/internal.ts index fa3b4990..e81fb27c 100644 --- a/packages/clarity-js/src/diagnostic/internal.ts +++ b/packages/clarity-js/src/diagnostic/internal.ts @@ -10,10 +10,10 @@ export function start(): void { } export function log(code: Code, severity: Severity, name: string = null, message: string = null, stack: string = null): void { - let key = name ? `${name}|${message}`: ""; + let key = name ? name + "|" + message: ""; // While rare, it's possible for code to fail repeatedly during the lifetime of the same page // In those cases, we only want to log the failure once and not spam logs with redundant information. - if (code in history && history[code].indexOf(key) >= 0) { return; } + if (code in history && history[code].includes(key)) { return; } data = { code, name, message, stack, severity }; diff --git a/packages/clarity-js/src/insight/encode.ts b/packages/clarity-js/src/insight/encode.ts index ff52f88f..e967a3a9 100644 --- a/packages/clarity-js/src/insight/encode.ts +++ b/packages/clarity-js/src/insight/encode.ts @@ -58,7 +58,7 @@ export default async function (type: Event): Promise { function attribute(key: string, value: string, privacy: Privacy, tag: string): string { if (key === Constant.Href && tag === Constant.LinkTag) { - return `${key}=${value}`; + return key + "=" + value; } - return `${key}=${scrub.text(value, key.indexOf(Constant.DataAttribute) === 0 ? Constant.DataAttribute : key, privacy)}`; + return key + "=" + scrub.text(value, key.indexOf(Constant.DataAttribute) === 0 ? Constant.DataAttribute : key, privacy); } \ No newline at end of file diff --git a/packages/clarity-js/src/insight/snapshot.ts b/packages/clarity-js/src/insight/snapshot.ts index e1a18915..d96f077d 100644 --- a/packages/clarity-js/src/insight/snapshot.ts +++ b/packages/clarity-js/src/insight/snapshot.ts @@ -76,7 +76,7 @@ function traverse(root: Node): void { case Node.ELEMENT_NODE: let element = node as HTMLElement; attributes = getAttributes(element); - tag = ["NOSCRIPT", "SCRIPT", "STYLE"].indexOf(element.tagName) < 0 ? element.tagName : tag; + tag = !["NOSCRIPT", "SCRIPT", "STYLE"].includes(element.tagName) ? element.tagName : tag; break; } add(node, parent, { tag, attributes, value }); diff --git a/packages/clarity-js/src/interaction/change.ts b/packages/clarity-js/src/interaction/change.ts index d6082539..4483e3c7 100644 --- a/packages/clarity-js/src/interaction/change.ts +++ b/packages/clarity-js/src/interaction/change.ts @@ -23,7 +23,7 @@ function recompute(evt: UIEvent): void { let element = target(evt) as HTMLInputElement; if (element) { let value = element.value; - let checksum = value && value.length >= Setting.WordLength && config.fraud && MaskExcludeList.indexOf(element.type) === -1 ? hash(value, Setting.ChecksumPrecision) : Constant.Empty; + let checksum = value && value.length >= Setting.WordLength && config.fraud && !MaskExcludeList.includes(element.type) ? hash(value, Setting.ChecksumPrecision) : Constant.Empty; state.push({ time: time(evt), event: Event.Change, data: { target: target(evt), type: element.type, value, checksum } }); schedule(encode.bind(this, Event.Change)); } diff --git a/packages/clarity-js/src/interaction/click.ts b/packages/clarity-js/src/interaction/click.ts index 8db7c67c..90ac32df 100644 --- a/packages/clarity-js/src/interaction/click.ts +++ b/packages/clarity-js/src/interaction/click.ts @@ -120,7 +120,7 @@ function text(element: Node): TextInfo { function reaction(element: Node): BooleanFlag { const tag = getElementAttribute(element, "tagName"); - if (UserInputTags.indexOf(tag) >= 0) { + if (UserInputTags.includes(tag)) { return BooleanFlag.False; } return BooleanFlag.True; @@ -216,11 +216,11 @@ function source(): ClickSource { let result = ClickSource.Unknown; for (const line of stack.split("\n")) { - if (line.indexOf("://") >= 0) { - result = line.indexOf("extension") < 0 && line.indexOf(origin) >= 0 + if (line.includes("://")) { + result = !line.includes("extension") && line.includes(origin) ? ClickSource.FirstParty : ClickSource.ThirdParty; - } else if (line.indexOf("eval") >= 0 || line.indexOf("Function") >= 0 || line.indexOf("= 0 || VM_PATTERN.test(line)) { + } else if (line.includes("eval") || line.includes("Function") || line.includes(" { tokens.push(entry.data.id); if (entry.data.isPrimary !== undefined) { - tokens.push(entry.data.isPrimary.toString()); + tokens.push("" + entry.data.isPrimary); } } queue(tokens); @@ -59,25 +59,16 @@ export default async function (type: Event, ts: number = null): Promise { let cTarget = metadata(entry.data.target as Node, entry.event, entry.data.text); tokens = [entry.time, entry.event]; let cHash = cTarget.hash ? cTarget.hash.join(Constant.Dot) : Constant.Empty; - tokens.push(cTarget.id); - tokens.push(entry.data.x); - tokens.push(entry.data.y); - tokens.push(entry.data.eX); - tokens.push(entry.data.eY); - tokens.push(entry.data.button); - tokens.push(entry.data.reaction); - tokens.push(entry.data.context); - tokens.push(scrub.text(entry.data.text, "click", cTarget.privacy)); - tokens.push(scrub.url(entry.data.link)); - tokens.push(cHash); - tokens.push(entry.data.trust); - tokens.push(entry.data.isFullText); - tokens.push(entry.data.w); - tokens.push(entry.data.h); - tokens.push(entry.data.tag); - tokens.push(entry.data.class); - tokens.push(entry.data.id); - tokens.push(entry.data.source); + tokens.push( + cTarget.id, entry.data.x, entry.data.y, + entry.data.eX, entry.data.eY, + entry.data.button, entry.data.reaction, entry.data.context, + scrub.text(entry.data.text, "click", cTarget.privacy), + scrub.url(entry.data.link), + cHash, entry.data.trust, entry.data.isFullText, + entry.data.w, entry.data.h, + entry.data.tag, entry.data.class, entry.data.id, entry.data.source + ); queue(tokens); timeline.track(entry.time, entry.event, cHash, entry.data.x, entry.data.y, entry.data.reaction, entry.data.context); } @@ -97,16 +88,14 @@ export default async function (type: Event, ts: number = null): Promise { break; case Event.Resize: let r = resize.data; - tokens.push(r.width); - tokens.push(r.height); + tokens.push(r.width, r.height); baseline.track(type, r.width, r.height); resize.reset(); queue(tokens); break; case Event.Unload: let u = unload.data; - tokens.push(u.name); - tokens.push(u.persisted); + tokens.push(u.name, u.persisted); unload.reset(); queue(tokens); break; @@ -126,10 +115,7 @@ export default async function (type: Event, ts: number = null): Promise { if (s) { let startTarget = metadata(s.start as Node, type); let endTarget = metadata(s.end as Node, type); - tokens.push(startTarget.id); - tokens.push(s.startOffset); - tokens.push(endTarget.id); - tokens.push(s.endOffset); + tokens.push(startTarget.id, s.startOffset, endTarget.id, s.endOffset); selection.reset(); queue(tokens); } @@ -143,12 +129,10 @@ export default async function (type: Event, ts: number = null): Promise { const sBottomHash = bottom?.hash ? bottom.hash.join(Constant.Dot) : Constant.Empty; if (sTarget.id > 0) { tokens = [entry.time, entry.event]; - tokens.push(sTarget.id); - tokens.push(entry.data.x); - tokens.push(entry.data.y); - tokens.push(sTopHash); - tokens.push(sBottomHash); - tokens.push(entry.data.trust); + tokens.push( + sTarget.id, entry.data.x, entry.data.y, + sTopHash, sBottomHash, entry.data.trust + ); queue(tokens); baseline.track(entry.event, entry.data.x, entry.data.y, entry.time); } @@ -184,12 +168,7 @@ export default async function (type: Event, ts: number = null): Promise { case Event.Timeline: for (let entry of timeline.updates) { tokens = [entry.time, entry.event]; - tokens.push(entry.data.type); - tokens.push(entry.data.hash); - tokens.push(entry.data.x); - tokens.push(entry.data.y); - tokens.push(entry.data.reaction); - tokens.push(entry.data.context); + tokens.push(entry.data.type, entry.data.hash, entry.data.x, entry.data.y, entry.data.reaction, entry.data.context); queue(tokens, false); } timeline.reset(); diff --git a/packages/clarity-js/src/layout/dom.ts b/packages/clarity-js/src/layout/dom.ts index 5f1c4d95..f8f33616 100644 --- a/packages/clarity-js/src/layout/dom.ts +++ b/packages/clarity-js/src/layout/dom.ts @@ -65,12 +65,12 @@ export function parse(root: ParentNode, init: boolean = false): void { // It's possible for script to receive invalid selectors, e.g. "'#id'" with extra quotes, and cause the code below to fail try { // Parse unmask configuration into separate query selectors and override tokens as part of initialization - if (init) { config.unmask.forEach(x => x.indexOf(Constant.Bang) < 0 ? unmask.push(x) : override.push(x.substr(1))); } + if (init) { config.unmask.forEach(x => !x.includes(Constant.Bang) ? unmask.push(x) : override.push(x.substr(1))); } // Since mutations may happen on leaf nodes too, e.g. text nodes, which may not support all selector APIs. // We ensure that the root note supports querySelectorAll API before executing the code below to identify new regions. if ("querySelectorAll" in root) { - config.regions.forEach(x => root.querySelectorAll(x[1]).forEach(e => region.observe(e, `${x[0]}`))); // Regions + config.regions.forEach(x => root.querySelectorAll(x[1]).forEach(e => region.observe(e, "" + x[0]))); // Regions config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements config.checksum.forEach(x => root.querySelectorAll(x[1]).forEach(e => fraudMap.set(e, x[0]))); // Fraud Checksum Check unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements @@ -237,18 +237,18 @@ function privacy(node: Node, value: NodeValue, parent: NodeValue): void { let tag = data.tag.toUpperCase(); switch (true) { - case maskTags.indexOf(tag) >= 0: + case maskTags.includes(tag): let type = attributes[Constant.Type]; let meta: string = Constant.Empty; const excludedPrivacyAttributes = [Constant.Class, Constant.Style] Object.keys(attributes) .filter((x) => !excludedPrivacyAttributes.includes(x as Constant)) .forEach((x) => (meta += attributes[x].toLowerCase())); - let exclude = maskExclude.some((x) => meta.indexOf(x) >= 0); + let exclude = maskExclude.some((x) => meta.includes(x)); // Regardless of privacy mode, always mask off user input from input boxes or drop downs with two exceptions: // (1) The node is detected to be one of the excluded fields, in which case we drop everything // (2) The node's type is one of the allowed types (like checkboxes) - metadata.privacy = tag === Constant.InputTag && maskDisable.indexOf(type) >= 0 ? current : (exclude ? Privacy.Exclude : Privacy.Text); + metadata.privacy = tag === Constant.InputTag && maskDisable.includes(type) ? current : (exclude ? Privacy.Exclude : Privacy.Text); break; case Constant.MaskData in attributes: metadata.privacy = Privacy.TextImage; @@ -269,7 +269,7 @@ function privacy(node: Node, value: NodeValue, parent: NodeValue): void { let pTag = parent && parent.data ? parent.data.tag : Constant.Empty; let pSelector = parent && parent.selector ? parent.selector[Selector.Default] : Constant.Empty; let tags: string[] = [Constant.StyleTag, Constant.TitleTag, Constant.SvgStyle]; - metadata.privacy = tags.includes(pTag) || override.some(x => pSelector.indexOf(x) >= 0) ? Privacy.None : current; + metadata.privacy = tags.includes(pTag) || override.some(x => pSelector.includes(x)) ? Privacy.None : current; break; case current === Privacy.Sensitive: // In a mode where we mask sensitive information by default, look through class names to aggressively mask content @@ -285,7 +285,7 @@ function privacy(node: Node, value: NodeValue, parent: NodeValue): void { } function inspect(input: string, lookup: string[], metadata: NodeMeta): Privacy { - if (input && lookup.some(x => input.indexOf(x) >= 0)) { + if (input && lookup.some(x => input.includes(x))) { return Privacy.Text; } return metadata.privacy; @@ -407,7 +407,7 @@ function updateImageSize(value: NodeValue): void { if(img && (!img.complete || img.naturalWidth === 0)){ // This will trigger mutation to update the original width and height after image loads. bind(img, 'load', () => { - img.setAttribute('data-clarity-loaded', `${shortid()}`); + img.setAttribute('data-clarity-loaded', "" + shortid()); }) } value.metadata.size = []; diff --git a/packages/clarity-js/src/layout/encode.ts b/packages/clarity-js/src/layout/encode.ts index 6fb0357c..89bc9579 100644 --- a/packages/clarity-js/src/layout/encode.ts +++ b/packages/clarity-js/src/layout/encode.ts @@ -22,18 +22,14 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu switch (type) { case Event.Document: let d = doc.data; - tokens.push(d.width); - tokens.push(d.height); + tokens.push(d.width, d.height); baseline.track(type, d.width, d.height); queue(tokens); break; case Event.Region: for (let r of region.state) { tokens = [r.time, Event.Region]; - tokens.push(r.data.id); - tokens.push(r.data.interaction); - tokens.push(r.data.visibility); - tokens.push(r.data.name); + tokens.push(r.data.id, r.data.interaction, r.data.visibility, r.data.name); queue(tokens, false); } region.reset(); @@ -42,16 +38,12 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu case Event.StyleSheetUpdate: for (let entry of style.sheetAdoptionState) { tokens = [entry.time, entry.event]; - tokens.push(entry.data.id); - tokens.push(entry.data.operation); - tokens.push(entry.data.newIds); + tokens.push(entry.data.id, entry.data.operation, entry.data.newIds); queue(tokens); } for (let entry of style.sheetUpdateState) { tokens = [entry.time, entry.event]; - tokens.push(entry.data.id); - tokens.push(entry.data.operation); - tokens.push(entry.data.cssRules); + tokens.push(entry.data.id, entry.data.operation, entry.data.cssRules); queue(tokens, false); } style.reset(); @@ -59,12 +51,11 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu case Event.Animation: for (let entry of animation.state) { tokens = [entry.time, entry.event]; - tokens.push(entry.data.id); - tokens.push(entry.data.operation); - tokens.push(entry.data.keyFrames); - tokens.push(entry.data.timing); - tokens.push(entry.data.timeline); - tokens.push(entry.data.targetId); + tokens.push( + entry.data.id, entry.data.operation, + entry.data.keyFrames, entry.data.timing, + entry.data.timeline, entry.data.targetId + ); queue(tokens); } animation.reset(); @@ -100,7 +91,7 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu if (value.previous) { tokens.push(value.previous); } } tokens.push(suspend ? Constant.SuspendMutationTag : data[key]); - if (box && box.length === 2) { tokens.push(`${Constant.Hash}${str(box[0])}.${str(box[1])}`); } + if (box && box.length === 2) { tokens.push(Constant.Hash + box[0].toString(36) + "." + box[1].toString(36)); } break; case "attributes": for (let attr in data[key]) { @@ -145,13 +136,9 @@ function size(value: NodeValue): number[] { return value.metadata.size; } -function str(input: number): string { - return input.toString(36); -} - function attribute(key: string, value: string, privacy: Privacy, tag: string): string { if (key === Constant.Href && tag === Constant.LinkTag) { - return `${key}=${value}`; + return key + "=" + value; } - return `${key}=${scrub.text(value, key.indexOf(Constant.DataAttribute) === 0 ? Constant.DataAttribute : key, privacy)}`; + return key + "=" + scrub.text(value, key.indexOf(Constant.DataAttribute) === 0 ? Constant.DataAttribute : key, privacy); } diff --git a/packages/clarity-js/src/layout/schema.ts b/packages/clarity-js/src/layout/schema.ts index e739468a..f53f3c07 100644 --- a/packages/clarity-js/src/layout/schema.ts +++ b/packages/clarity-js/src/layout/schema.ts @@ -12,7 +12,7 @@ export function ld(json: any): void { if (key === JsonLD.Type && typeof value === "string") { value = value.toLowerCase(); /* Normalizations */ - value = value.indexOf(JsonLD.Article) >= 0 || value.indexOf(JsonLD.Posting) >= 0 ? JsonLD.Article : value; + value = value.includes(JsonLD.Article) || value.includes(JsonLD.Posting) ? JsonLD.Article : value; switch (value) { case JsonLD.Article: case JsonLD.Recipe: diff --git a/packages/clarity-js/src/layout/selector.ts b/packages/clarity-js/src/layout/selector.ts index 25dd7267..4dcb5416 100644 --- a/packages/clarity-js/src/layout/selector.ts +++ b/packages/clarity-js/src/layout/selector.ts @@ -12,7 +12,7 @@ export function reset(): void { export function get(input: SelectorInput, type: Selector): string { let a = input.attributes; let prefix = input.prefix ? input.prefix[type] : null; - let suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position-1}` : `:nth-of-type(${input.position})`; + let suffix = type === Selector.Alpha ? Constant.Tilde + (input.position-1) : ":nth-of-type(" + input.position + ")"; switch (input.tag) { case "STYLE": case "TITLE": @@ -25,35 +25,35 @@ export function get(input: SelectorInput, type: Selector): string { return Constant.HTML; default: if (prefix === null) { return Constant.Empty; } - prefix = `${prefix}${Constant.Separator}`; + prefix = prefix + Constant.Separator; input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag; - let selector = `${prefix}${input.tag}${suffix}`; + let selector = prefix + input.tag + suffix; let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null; let classes = input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/).filter(c => filter(c)).join(Constant.Period) : null; if (classes && classes.length > 0) { if (type === Selector.Alpha) { // In Alpha mode, update selector to use class names, with relative positioning within the parent id container. // If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple. - let key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`; + let key = getDomPath(prefix) + input.tag + Constant.Dot + classes; if (!(key in selectorMap)) { selectorMap[key] = []; } - if (selectorMap[key].indexOf(input.id) < 0) { selectorMap[key].push(input.id); } - selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`; + if (!selectorMap[key].includes(input.id)) { selectorMap[key].push(input.id); } + selector = key + Constant.Tilde + selectorMap[key].indexOf(input.id); } else { // In Beta mode, we continue to look at query selectors in context of the full page - selector = `${prefix}${input.tag}.${classes}${suffix}` + selector = prefix + input.tag + "." + classes + suffix } } // Update selector to use "id" field when available. There are two exceptions: // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts - selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector; + selector = id && filter(id) ? getDomPrefix(prefix) + Constant.Hash + id : selector; return selector; } } function getDomPrefix(prefix: string): string { const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag); - const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`); + const iframeDomStart = prefix.lastIndexOf(Constant.IFramePrefix + Constant.HTML); const domStart = Math.max(shadowDomStart, iframeDomStart); if (domStart < 0) { return Constant.Empty; } @@ -74,7 +74,7 @@ function getDomPath(input: string): string { // Check if the given input string has digits or excluded class names function filter(value: string): boolean { if (!value) { return false; } // Do not process empty strings - if (excludeClassNames.some(x => value.toLowerCase().indexOf(x) >= 0)) { return false; } + if (excludeClassNames.some(x => value.toLowerCase().includes(x))) { return false; } for (let i = 0; i < value.length; i++) { let c = value.charCodeAt(i); if (c >= Character.Zero && c <= Character.Nine) { return false }; diff --git a/packages/clarity-js/src/performance/encode.ts b/packages/clarity-js/src/performance/encode.ts index c030a634..230becb9 100644 --- a/packages/clarity-js/src/performance/encode.ts +++ b/packages/clarity-js/src/performance/encode.ts @@ -8,22 +8,16 @@ export default async function(type: Event): Promise { let tokens: Token[] = [t, type]; switch (type) { case Event.Navigation: - tokens.push(navigation.data.fetchStart); - tokens.push(navigation.data.connectStart); - tokens.push(navigation.data.connectEnd); - tokens.push(navigation.data.requestStart); - tokens.push(navigation.data.responseStart); - tokens.push(navigation.data.responseEnd); - tokens.push(navigation.data.domInteractive); - tokens.push(navigation.data.domComplete); - tokens.push(navigation.data.loadEventStart); - tokens.push(navigation.data.loadEventEnd); - tokens.push(navigation.data.redirectCount); - tokens.push(navigation.data.size); - tokens.push(navigation.data.type); - tokens.push(navigation.data.protocol); - tokens.push(navigation.data.encodedSize); - tokens.push(navigation.data.decodedSize); + tokens.push( + navigation.data.fetchStart, navigation.data.connectStart, + navigation.data.connectEnd, navigation.data.requestStart, + navigation.data.responseStart, navigation.data.responseEnd, + navigation.data.domInteractive, navigation.data.domComplete, + navigation.data.loadEventStart, navigation.data.loadEventEnd, + navigation.data.redirectCount, navigation.data.size, + navigation.data.type, navigation.data.protocol, + navigation.data.encodedSize, navigation.data.decodedSize + ); navigation.reset(); queue(tokens); break; diff --git a/packages/clarity-js/src/performance/observer.ts b/packages/clarity-js/src/performance/observer.ts index a5104db1..d0a4d351 100644 --- a/packages/clarity-js/src/performance/observer.ts +++ b/packages/clarity-js/src/performance/observer.ts @@ -40,7 +40,7 @@ function observe(): void { // It must only be used only with the "type" option, and cannot be used with entryTypes. // This is why we need to individually "observe" each supported type for (let x of types) { - if (PerformanceObserver.supportedEntryTypes.indexOf(x) >= 0) { + if (PerformanceObserver.supportedEntryTypes.includes(x)) { // Initialize CLS with a value of zero. It's possible (and recommended) for sites to not have any cumulative layout shift. // In those cases, we want to still initialize the metric in Clarity if (x === Constant.CLS) { metric.sum(Metric.CumulativeLayoutShift, 0); } @@ -78,7 +78,7 @@ function process(entries: PerformanceEntryList): void { { interaction.processInteractionEntry(entry as PerformanceEventTiming); // Logging it as dimension because we're always looking for the last value. - dimension.log(Dimension.InteractionNextPaint, interaction.estimateP98LongestInteraction().toString()); + dimension.log(Dimension.InteractionNextPaint, "" + interaction.estimateP98LongestInteraction()); } break; case Constant.CLS: From c7305fc2b6068a3f2bf571b015d02445b0fe6dcc Mon Sep 17 00:00:00 2001 From: Manu Nair Date: Sun, 29 Mar 2026 16:59:52 -0700 Subject: [PATCH 5/8] fix: restore ordering comments and missing trailing newlines Restore important comments removed during refactoring that document module ordering constraints and inter-module dependencies. Add missing trailing newlines to files modified in earlier refactor commits. Co-Authored-By: Claude Opus 4.6 --- packages/clarity-js/src/data/index.ts | 5 +++++ packages/clarity-js/src/insight/encode.ts | 2 +- packages/clarity-js/src/interaction/index.ts | 2 ++ packages/clarity-js/src/layout/index.ts | 3 +++ packages/clarity-js/src/layout/mutation.ts | 2 +- packages/clarity-js/src/layout/style.ts | 2 +- 6 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/clarity-js/src/data/index.ts b/packages/clarity-js/src/data/index.ts index 9c3da88e..13803539 100644 --- a/packages/clarity-js/src/data/index.ts +++ b/packages/clarity-js/src/data/index.ts @@ -39,11 +39,16 @@ const modules: Module[] = [ ]; export function start(): void { + // Metric needs to be initialized before we can start measuring. so metric is not wrapped in measure metric.start(); modules.forEach(x => measure(x.start)()); } export function stop(): void { + // Stop modules in the reverse order of their initialization + // The ordering below should respect inter-module dependency. + // E.g. if upgrade depends on upload, then upgrade needs to end before upload. + // Similarly, if upload depends on metadata, upload needs to end before metadata. modules.slice().reverse().forEach(x => measure(x.stop)()); metric.stop(); } diff --git a/packages/clarity-js/src/insight/encode.ts b/packages/clarity-js/src/insight/encode.ts index e967a3a9..5017711b 100644 --- a/packages/clarity-js/src/insight/encode.ts +++ b/packages/clarity-js/src/insight/encode.ts @@ -61,4 +61,4 @@ function attribute(key: string, value: string, privacy: Privacy, tag: string): s return key + "=" + value; } return key + "=" + scrub.text(value, key.indexOf(Constant.DataAttribute) === 0 ? Constant.DataAttribute : key, privacy); -} \ No newline at end of file +} diff --git a/packages/clarity-js/src/interaction/index.ts b/packages/clarity-js/src/interaction/index.ts index 9cc2deda..23e50755 100644 --- a/packages/clarity-js/src/interaction/index.ts +++ b/packages/clarity-js/src/interaction/index.ts @@ -49,6 +49,8 @@ export function stop(): void { export function observe(root: Node): void { scrObs(root); + // Only monitor following interactions if the root node is a document. + // In case of shadow DOM, following events automatically bubble up to the parent document. if (root.nodeType === Node.DOCUMENT_NODE) { clkObs(root); cbObs(root); diff --git a/packages/clarity-js/src/layout/index.ts b/packages/clarity-js/src/layout/index.ts index 59822349..a674efba 100644 --- a/packages/clarity-js/src/layout/index.ts +++ b/packages/clarity-js/src/layout/index.ts @@ -12,6 +12,7 @@ import config from "@src/core/config"; export { hashText } from "@src/layout/dom"; export function start(): void { + // The order below is important and is determined by interdependencies of modules docStart(); regStart(); domStart(); @@ -22,6 +23,8 @@ export function start(): void { } else { mutStart(); } + // IMPORTANT: Start custom element detection BEFORE discover + // This ensures pre-existing custom elements are registered before DOM traversal custStart(); discStart(); styStart(); diff --git a/packages/clarity-js/src/layout/mutation.ts b/packages/clarity-js/src/layout/mutation.ts index 898ae4b5..760ea468 100644 --- a/packages/clarity-js/src/layout/mutation.ts +++ b/packages/clarity-js/src/layout/mutation.ts @@ -382,4 +382,4 @@ function proxyRule(win: any, cls: string, key: string, method: string, getNode: return win.__clr[key].apply(this, arguments); }; } -} \ No newline at end of file +} diff --git a/packages/clarity-js/src/layout/style.ts b/packages/clarity-js/src/layout/style.ts index 88594242..9a884e0c 100644 --- a/packages/clarity-js/src/layout/style.ts +++ b/packages/clarity-js/src/layout/style.ts @@ -140,4 +140,4 @@ function trackStyleAdoption(time: number, id: number, operation: StyleSheetOpera function arraysEqual(a: string[], b: string[]): boolean { return a.length === b.length && a.every((v, i) => v === b[i]); -} \ No newline at end of file +} From d4e7417f53e18d07bc34344e67abe2e4a7d1c419 Mon Sep 17 00:00:00 2001 From: Manu Nair Date: Sun, 29 Mar 2026 17:30:52 -0700 Subject: [PATCH 6/8] fix formatting --- packages/clarity-js/src/layout/node.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/clarity-js/src/layout/node.ts b/packages/clarity-js/src/layout/node.ts index f3e1668b..ec09caf8 100644 --- a/packages/clarity-js/src/layout/node.ts +++ b/packages/clarity-js/src/layout/node.ts @@ -1,6 +1,9 @@ import { Constant, Source } from "@clarity-types/layout"; import { Code, Dimension, Severity } from "@clarity-types/data"; -import { add as domAdd, update as domUpdate, has as domHas, iframe as domIframe, get as domGet, parse as domParse, sameorigin, iframeContent, removeIFrame } from "./dom"; +import { + add as domAdd, update as domUpdate, has as domHas, iframe as domIframe, + get as domGet, parse as domParse, sameorigin, iframeContent, removeIFrame +} from "./dom"; import * as event from "@src/core/event"; import * as dimension from "@src/data/dimension"; import * as internal from "@src/diagnostic/internal"; From 9a2ee5d6bf4a0e4bfbb19c8af095b9615610757f Mon Sep 17 00:00:00 2001 From: Manu Nair Date: Fri, 3 Apr 2026 16:21:20 -0700 Subject: [PATCH 7/8] bump version --- lerna.json | 2 +- package.json | 2 +- packages/clarity-decode/package.json | 4 ++-- packages/clarity-devtools/package.json | 8 ++++---- packages/clarity-devtools/static/manifest.json | 4 ++-- packages/clarity-js/package.json | 2 +- packages/clarity-js/src/core/version.ts | 2 +- packages/clarity-visualize/package.json | 4 ++-- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lerna.json b/lerna.json index 65fe017d..b700701d 100644 --- a/lerna.json +++ b/lerna.json @@ -2,6 +2,6 @@ "packages": [ "packages/*" ], - "version": "0.8.59", + "version": "0.8.60", "npmClient": "yarn" } \ No newline at end of file diff --git a/package.json b/package.json index 49b438dc..405a3d62 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "clarity", "private": true, - "version": "0.8.59", + "version": "0.8.60", "repository": "https://github.com/microsoft/clarity.git", "author": "Sarvesh Nagpal ", "license": "MIT", diff --git a/packages/clarity-decode/package.json b/packages/clarity-decode/package.json index 99b6f212..df738dbf 100644 --- a/packages/clarity-decode/package.json +++ b/packages/clarity-decode/package.json @@ -1,6 +1,6 @@ { "name": "clarity-decode", - "version": "0.8.59", + "version": "0.8.60", "description": "An analytics library that uses web page interactions to generate aggregated insights", "author": "Microsoft Corp.", "license": "MIT", @@ -26,7 +26,7 @@ "url": "https://github.com/Microsoft/clarity/issues" }, "dependencies": { - "clarity-js": "^0.8.59" + "clarity-js": "^0.8.60" }, "devDependencies": { "@rollup/plugin-commonjs": "^24.0.0", diff --git a/packages/clarity-devtools/package.json b/packages/clarity-devtools/package.json index 898b96be..c2abe0e6 100644 --- a/packages/clarity-devtools/package.json +++ b/packages/clarity-devtools/package.json @@ -1,6 +1,6 @@ { "name": "clarity-devtools", - "version": "0.8.59", + "version": "0.8.60", "private": true, "description": "Adds Clarity debugging support to browser devtools", "author": "Microsoft Corp.", @@ -24,9 +24,9 @@ "url": "https://github.com/Microsoft/clarity/issues" }, "dependencies": { - "clarity-decode": "^0.8.59", - "clarity-js": "^0.8.59", - "clarity-visualize": "^0.8.59" + "clarity-decode": "^0.8.60", + "clarity-js": "^0.8.60", + "clarity-visualize": "^0.8.60" }, "devDependencies": { "@rollup/plugin-node-resolve": "^15.0.0", diff --git a/packages/clarity-devtools/static/manifest.json b/packages/clarity-devtools/static/manifest.json index db9042e1..d89b0e17 100644 --- a/packages/clarity-devtools/static/manifest.json +++ b/packages/clarity-devtools/static/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "Microsoft Clarity Developer Tools", "description": "Clarity helps you understand how users are interacting with your website.", - "version": "0.8.59", - "version_name": "0.8.59", + "version": "0.8.60", + "version_name": "0.8.60", "minimum_chrome_version": "88", "devtools_page": "devtools.html", "icons": { diff --git a/packages/clarity-js/package.json b/packages/clarity-js/package.json index 00b6f841..ccadfe65 100644 --- a/packages/clarity-js/package.json +++ b/packages/clarity-js/package.json @@ -1,6 +1,6 @@ { "name": "clarity-js", - "version": "0.8.59", + "version": "0.8.60", "description": "An analytics library that uses web page interactions to generate aggregated insights", "author": "Microsoft Corp.", "license": "MIT", diff --git a/packages/clarity-js/src/core/version.ts b/packages/clarity-js/src/core/version.ts index b790252a..beac9198 100644 --- a/packages/clarity-js/src/core/version.ts +++ b/packages/clarity-js/src/core/version.ts @@ -1,2 +1,2 @@ -let version = "0.8.59"; +let version = "0.8.60"; export default version; diff --git a/packages/clarity-visualize/package.json b/packages/clarity-visualize/package.json index a1d1f8e9..0e64493d 100644 --- a/packages/clarity-visualize/package.json +++ b/packages/clarity-visualize/package.json @@ -1,6 +1,6 @@ { "name": "clarity-visualize", - "version": "0.8.59", + "version": "0.8.60", "description": "An analytics library that uses web page interactions to generate aggregated insights", "author": "Microsoft Corp.", "license": "MIT", @@ -27,7 +27,7 @@ "url": "https://github.com/Microsoft/clarity/issues" }, "dependencies": { - "clarity-decode": "^0.8.59" + "clarity-decode": "^0.8.60" }, "devDependencies": { "@rollup/plugin-commonjs": "^24.0.0", From 89b1a1dbdec54c57e38131fd18ee2a9ba66577e0 Mon Sep 17 00:00:00 2001 From: Manu Nair Date: Mon, 6 Apr 2026 17:20:02 -0700 Subject: [PATCH 8/8] review comments --- packages/clarity-js/src/data/compress.ts | 2 ++ packages/clarity-js/src/layout/animation.ts | 1 + packages/clarity-js/src/layout/mutation.ts | 8 +++-- packages/clarity-js/src/layout/node.ts | 34 +++++++++------------ packages/clarity-js/src/layout/style.ts | 5 ++- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/clarity-js/src/data/compress.ts b/packages/clarity-js/src/data/compress.ts index 08d122d5..fbe9cf3e 100644 --- a/packages/clarity-js/src/data/compress.ts +++ b/packages/clarity-js/src/data/compress.ts @@ -5,6 +5,8 @@ const supported = Constant.CompressionStream in window; export default async function(input: string): Promise { try { if (supported) { + // Create a readable stream from given input string and + // pipe it through text encoder and compression stream to gzip it const stream = new ReadableStream({async start(controller) { controller.enqueue(input); controller.close(); diff --git a/packages/clarity-js/src/layout/animation.ts b/packages/clarity-js/src/layout/animation.ts index 83e77fbc..0058d21d 100644 --- a/packages/clarity-js/src/layout/animation.ts +++ b/packages/clarity-js/src/layout/animation.ts @@ -78,6 +78,7 @@ export function stop(): void { function overrideAnimationHelper(name: string) { let original = Animation.prototype[name]; + if (typeof original !== "function") { return; } Animation.prototype[name] = function(): void { trackAnimationOperation(this, name); return original.apply(this, arguments); diff --git a/packages/clarity-js/src/layout/mutation.ts b/packages/clarity-js/src/layout/mutation.ts index 760ea468..e1ff8c99 100644 --- a/packages/clarity-js/src/layout/mutation.ts +++ b/packages/clarity-js/src/layout/mutation.ts @@ -348,14 +348,18 @@ function proxyStyleRules(win: any): void { win.__clr = win.__clr || {}; - // Proxy insertRule/deleteRule on CSSStyleSheet and CSSMediaRule to detect dynamic style changes. - // Libraries like styled-components use insertRule API instead of DOM text nodes. + // Some popular open source libraries, like styled-components, optimize performance + // by injecting CSS using insertRule API vs. appending text node. A side effect of + // using javascript API is that it doesn't trigger DOM mutation and therefore we + // need to override the insertRule API and listen for changes manually. proxyRule(win, "CSSStyleSheet", "InsertRule", "insertRule", function() { return this.ownerNode; }); proxyRule(win, "CSSStyleSheet", "DeleteRule", "deleteRule", function() { return this.ownerNode; }); proxyRule(win, "CSSMediaRule", "MediaInsertRule", "insertRule", function() { return this.parentStyleSheet.ownerNode; }); proxyRule(win, "CSSMediaRule", "MediaDeleteRule", "deleteRule", function() { return this.parentStyleSheet.ownerNode; }); // Add a hook to attachShadow API calls + // In case we are unable to add a hook and browser throws an exception, + // reset attachShadow variable and resume processing like before if ("Element" in win && win.Element && win.Element.prototype && win.__clr.AttachShadow === undefined) { win.__clr.AttachShadow = win.Element.prototype.attachShadow; try { diff --git a/packages/clarity-js/src/layout/node.ts b/packages/clarity-js/src/layout/node.ts index ec09caf8..69da0855 100644 --- a/packages/clarity-js/src/layout/node.ts +++ b/packages/clarity-js/src/layout/node.ts @@ -17,10 +17,6 @@ import { electron } from "@src/data/metadata"; const IGNORE_ATTRIBUTES = ["title", "alt", "onload", "onfocus", "onerror", "data-drupal-form-submit-last", "aria-label"]; const newlineRegex = /[\r\n]+/g; -function domCall(isAdd: boolean, node: Node, parent: Node, data: any, source: Source): void { - isAdd ? domAdd(node, parent, data, source) : domUpdate(node, parent, data, source); -} - export default function (node: Node, source: Source, timestamp: number): Node { let child: Node = null; @@ -35,7 +31,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { node = node.parentNode; } - let isAdd = domHas(node) === false; + let domFn = domHas(node) ? domUpdate : domAdd; let parent = node.parentElement ? node.parentElement : null; let insideFrame = node.ownerDocument !== document; switch (node.nodeType) { @@ -46,7 +42,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { let docName = doctype.name ? doctype.name : Constant.HTML; let docAttributes = { name: docName, publicId: doctype.publicId, systemId: doctype.systemId }; let docData = { tag: docTypePrefix + Constant.DocumentTag, attributes: docAttributes }; - domCall(isAdd, node, parent, docData, source); + domFn(node, parent, docData, source); break; case Node.DOCUMENT_NODE: // We check for regions in the beginning when discovering document and @@ -71,12 +67,12 @@ export default function (node: Node, source: Source, timestamp: number): Node { // cause any unintended side effect to the page. We will re-evaluate after we gather more real world data on this. let style = Constant.Empty as string; let fragmentData = { tag: Constant.ShadowDomTag, attributes: { style } }; - domCall(isAdd, node, shadowRoot.host, fragmentData, source); + domFn(node, shadowRoot.host, fragmentData, source); } else { // If the browser doesn't support shadow DOM natively, we detect that, and send appropriate tag back. // The differentiation is important because we don't have to observe pollyfill shadow DOM nodes, // the same way we observe real shadow DOM nodes (encapsulation provided by the browser). - domCall(isAdd, node, shadowRoot.host, { tag: Constant.PolyfillShadowDomTag, attributes: {} }, source); + domFn(node, shadowRoot.host, { tag: Constant.PolyfillShadowDomTag, attributes: {} }, source); } checkDocumentStyles(node as Document, timestamp); } @@ -89,9 +85,9 @@ export default function (node: Node, source: Source, timestamp: number): Node { // Also, we do not track text nodes for STYLE tags // The only exception is when we receive a mutation to remove the text node, in that case // parent will be null, but we can still process the node by checking it's an update call. - if (!isAdd || (parent && domHas(parent) && parent.tagName !== "STYLE" && parent.tagName !== "NOSCRIPT")) { + if (domFn === domUpdate || (parent && domHas(parent) && parent.tagName !== "STYLE" && parent.tagName !== "NOSCRIPT")) { let textData = { tag: Constant.TextTag, value: node.nodeValue }; - domCall(isAdd, node, parent, textData, source); + domFn(node, parent, textData, source); } break; case Node.ELEMENT_NODE: @@ -109,7 +105,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { parent = insideFrame && parent ? domIframe(parent) : parent; let htmlPrefix = insideFrame ? Constant.IFramePrefix : Constant.Empty; let htmlData = { tag: htmlPrefix + tag, attributes }; - domCall(isAdd, node, parent, htmlData, source); + domFn(node, parent, htmlData, source); break; case "SCRIPT": if (Constant.Type in attributes && attributes[Constant.Type] === Constant.JsonLD) { @@ -122,7 +118,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { // keeping the noscript tag but ignoring its contents. Some HTML markup relies on having these tags // to maintain parity with the original css view, but we don't want to execute any noscript in Clarity let noscriptData = { tag, attributes: {}, value: '' }; - domCall(isAdd, node, parent, noscriptData, source); + domFn(node, parent, noscriptData, source); break; case "META": var key = (Constant.Property in attributes ? @@ -147,7 +143,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { let head = { tag, attributes }; let l = insideFrame && node.ownerDocument?.location ? node.ownerDocument.location : location; head.attributes[Constant.Base] = l.protocol + "//" + l.host + l.pathname; - domCall(isAdd, node, parent, head, source); + domFn(node, parent, head, source); break; case "BASE": // Override the auto detected base path to explicit value specified in this tag @@ -161,7 +157,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { break; case "STYLE": let styleData = { tag, attributes, value: getStyleValue(element as HTMLStyleElement) }; - domCall(isAdd, node, parent, styleData, source); + domFn(node, parent, styleData, source); break; case "IFRAME": let iframe = node as HTMLIFrameElement; @@ -176,7 +172,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { if (source === Source.ChildListRemove) { removeObserver(iframe); } - domCall(isAdd, node, parent, frameData, source); + domFn(node, parent, frameData, source); break; case "LINK": // electron stylesheets reference the local file system - translating those @@ -186,7 +182,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { var currentStyleSheet = document.styleSheets[styleSheetIndex]; if (currentStyleSheet.ownerNode == element) { let syntheticStyleData = { tag: "STYLE", attributes, value: getCssRules(currentStyleSheet) }; - domCall(isAdd, node, parent, syntheticStyleData, source); + domFn(node, parent, syntheticStyleData, source); break; } } @@ -194,7 +190,7 @@ export default function (node: Node, source: Source, timestamp: number): Node { } // for links that aren't electron style sheets we can process them normally let linkData = { tag, attributes }; - domCall(isAdd, node, parent, linkData, source); + domFn(node, parent, linkData, source); break; case "VIDEO": case "AUDIO": @@ -204,13 +200,13 @@ export default function (node: Node, source: Source, timestamp: number): Node { attributes[Constant.Src] = ""; } let mediaTag = { tag, attributes }; - domCall(isAdd, node, parent, mediaTag, source); + domFn(node, parent, mediaTag, source); break; default: custom.check(element.localName); let data = { tag, attributes }; if (element.shadowRoot) { child = element.shadowRoot; } - domCall(isAdd, node, parent, data, source); + domFn(node, parent, data, source); break; } break; diff --git a/packages/clarity-js/src/layout/style.ts b/packages/clarity-js/src/layout/style.ts index 9a884e0c..93592e16 100644 --- a/packages/clarity-js/src/layout/style.ts +++ b/packages/clarity-js/src/layout/style.ts @@ -29,10 +29,13 @@ function proxyStyleRules(win: any) { } } +// If we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it +// and attached the sheet to a document. This way the timestamp of the style sheet creation will align +// to when it is used in the document rather than potentially being misaligned during the traverse process. function proxyStyleMethod(win: any, method: string, operation: StyleSheetOperation): void { if (win.__clr[method] === undefined) { win.__clr[method] = win.CSSStyleSheet.prototype[method]; - win.CSSStyleSheet.prototype[method] = function(): any { + win.CSSStyleSheet.prototype[method] = function() { if (core.active()) { if (createdSheetIds.includes(this[styleSheetId])) { trackStyleChange(time(), this[styleSheetId], operation, arguments[0]);