diff --git a/packages/clarity-decode/package.json b/packages/clarity-decode/package.json index cca962be..8a6270a5 100644 --- a/packages/clarity-decode/package.json +++ b/packages/clarity-decode/package.json @@ -1,6 +1,6 @@ { "name": "clarity-decode", - "version": "0.7.25", + "version": "0.7.26", "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.7.25" + "clarity-js": "^0.7.26" }, "devDependencies": { "@rollup/plugin-commonjs": "^24.0.0", diff --git a/packages/clarity-devtools/package.json b/packages/clarity-devtools/package.json index 6c354a75..da1ffc5c 100644 --- a/packages/clarity-devtools/package.json +++ b/packages/clarity-devtools/package.json @@ -1,6 +1,6 @@ { "name": "clarity-devtools", - "version": "0.7.25", + "version": "0.7.26", "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.7.25", - "clarity-js": "^0.7.25", - "clarity-visualize": "^0.7.25" + "clarity-decode": "^0.7.26", + "clarity-js": "^0.7.26", + "clarity-visualize": "^0.7.26" }, "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 8932cb98..eb5f7439 100644 --- a/packages/clarity-devtools/static/manifest.json +++ b/packages/clarity-devtools/static/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "Microsoft Clarity Developer Tools", "description": "Clarity helps you understand how users are interacting with your website.", - "version": "0.7.25", - "version_name": "0.7.25", + "version": "0.7.26", + "version_name": "0.7.26", "minimum_chrome_version": "50", "devtools_page": "devtools.html", "icons": { diff --git a/packages/clarity-js/package.json b/packages/clarity-js/package.json index 6d1a4cf1..7239cdd0 100644 --- a/packages/clarity-js/package.json +++ b/packages/clarity-js/package.json @@ -1,6 +1,6 @@ { "name": "clarity-js", - "version": "0.7.25", + "version": "0.7.26", "description": "An analytics library that uses web page interactions to generate aggregated insights", "author": "Microsoft Corp.", "license": "MIT", diff --git a/packages/clarity-js/rollup.config.ts b/packages/clarity-js/rollup.config.ts index bf2007a6..d07f0523 100644 --- a/packages/clarity-js/rollup.config.ts +++ b/packages/clarity-js/rollup.config.ts @@ -15,7 +15,7 @@ export default [ plugins: [ alias({ entries: [ - { find: '@src/layout/style', replacement: '@src/insight/blank' } + { find: '@src/layout/style', replacement: '@src/insight/style' } ] }), resolve(), diff --git a/packages/clarity-js/src/core/version.ts b/packages/clarity-js/src/core/version.ts index 63525337..977f0383 100644 --- a/packages/clarity-js/src/core/version.ts +++ b/packages/clarity-js/src/core/version.ts @@ -1,2 +1,2 @@ -let version = "0.7.25"; +let version = "0.7.26"; export default version; diff --git a/packages/clarity-js/src/insight/style.ts b/packages/clarity-js/src/insight/style.ts new file mode 100644 index 00000000..965c6ae7 --- /dev/null +++ b/packages/clarity-js/src/insight/style.ts @@ -0,0 +1,52 @@ +import { Metric } from "@clarity-types/data"; +import { StyleSheetState } from "@clarity-types/layout"; +import * as core from "@src/core"; +import * as metric from "@src/data/metric"; + +export let state: StyleSheetState[] = []; +let replace: (text?: string) => Promise = null; +let replaceSync: (text?: string) => void = null; + +export function start(): void { + reset(); + + if (replace === null) { + replace = CSSStyleSheet.prototype.replace; + CSSStyleSheet.prototype.replace = function(): Promise { + if (core.active()) { + metric.max(Metric.ConstructedStyles, 1); + } + return replace.apply(this, arguments); + }; + } + + if (replaceSync === null) { + replaceSync = CSSStyleSheet.prototype.replaceSync; + CSSStyleSheet.prototype.replaceSync = function(): void { + if (core.active()) { + metric.max(Metric.ConstructedStyles, 1); + } + return replaceSync.apply(this, arguments); + }; + } +} + +export function checkDocumentStyles(documentNode: Document): void { + if (!documentNode?.adoptedStyleSheets) { + // if we don't have adoptedStyledSheets on the Node passed to us, we can short circuit. + return; + } + metric.max(Metric.ConstructedStyles, 1); +} + +export function compute(): void { + checkDocumentStyles(document); +} + +export function reset(): void { + state = []; +} + +export function stop(): void { + reset(); +} diff --git a/packages/clarity-js/src/layout/node.ts b/packages/clarity-js/src/layout/node.ts index 0eb3bc75..338c79d6 100644 --- a/packages/clarity-js/src/layout/node.ts +++ b/packages/clarity-js/src/layout/node.ts @@ -90,12 +90,6 @@ export default function (node: Node, source: Source): Node { // In some cases, external libraries like vue-fragment, can modify parentNode property to not be in sync with the DOM // For correctness, we first look at parentElement and if it not present then fall back to using parentNode parent = node.parentElement ? node.parentElement : (node.parentNode ? node.parentNode as HTMLElement : null); - // For HTML slots, the parentElement doesn't actually match what is rendered. If we have an assignedSlot, we use that - // as the parent to ensure our visualizations match the browser behavior for end users. - var slottedParent = (node as HTMLElement).assignedSlot; - if (slottedParent) { - parent = slottedParent; - } // If we encounter a node that is part of SVG namespace, prefix the tag with SVG_PREFIX if (element.namespaceURI === Constant.SvgNamespace) { tag = Constant.SvgPrefix + tag; } diff --git a/packages/clarity-js/src/layout/style.ts b/packages/clarity-js/src/layout/style.ts index d1537d64..1cdb8138 100644 --- a/packages/clarity-js/src/layout/style.ts +++ b/packages/clarity-js/src/layout/style.ts @@ -1,16 +1,18 @@ -import { Event } from "@clarity-types/data"; +import { Event, Metric } from "@clarity-types/data"; import { StyleSheetOperation, StyleSheetState } from "@clarity-types/layout"; import { time } from "@src/core/time"; -import { shortid } from "@src/data/metadata"; +import { shortid, data as metadataFields } from "@src/data/metadata"; import encode from "@src/layout/encode"; import { getId, getNode } from "@src/layout/dom"; import * as core from "@src/core"; import { getCssRules } from "./node"; +import * as metric from "@src/data/metric"; export let state: StyleSheetState[] = []; let replace: (text?: string) => Promise = null; let replaceSync: (text?: string) => void = null; const styleSheetId = 'claritySheetId'; +const styleSheetPageNum = 'claritySheetNum'; let styleSheetMap = {}; export function start(): void { @@ -20,12 +22,8 @@ export function start(): void { replace = CSSStyleSheet.prototype.replace; CSSStyleSheet.prototype.replace = function(): Promise { if (core.active()) { - if (!this[styleSheetId]) { - this[styleSheetId] = shortid(); - // need to pass a create style sheet event (don't add it to any nodes, but do create it) - trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Create); - } - + metric.max(Metric.ConstructedStyles, 1); + bootStrapStyleSheet(this); trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Replace, arguments[0]); } return replace.apply(this, arguments); @@ -36,11 +34,8 @@ export function start(): void { replaceSync = CSSStyleSheet.prototype.replaceSync; CSSStyleSheet.prototype.replaceSync = function(): void { if (core.active()) { - if (!this[styleSheetId]) { - this[styleSheetId] = shortid(); - // need to pass a create style sheet event (don't add it to any nodes, but do create it) - trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Create); - } + metric.max(Metric.ConstructedStyles, 1); + bootStrapStyleSheet(this); trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.ReplaceSync, arguments[0]); } return replaceSync.apply(this, arguments); @@ -48,15 +43,31 @@ export function start(): void { } } +function bootStrapStyleSheet(styleSheet: CSSStyleSheet): void { + // If we haven't seen this style sheet on this page yet, we create a reference to it for the visualizer. + // 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. + const pageNum = metadataFields.pageNum; + if (styleSheet[styleSheetPageNum] !== pageNum) { + styleSheet[styleSheetPageNum] = pageNum; + styleSheet[styleSheetId] = shortid(); + // need to pass a create style sheet event (don't add it to any nodes, but do create it) + trackStyleChange(time(), styleSheet[styleSheetId], StyleSheetOperation.Create); + } +} + export function checkDocumentStyles(documentNode: Document): void { if (!documentNode?.adoptedStyleSheets) { // if we don't have adoptedStyledSheets on the Node passed to us, we can short circuit. return; } + metric.max(Metric.ConstructedStyles, 1); let currentStyleSheets: string[] = []; for (var styleSheet of documentNode.adoptedStyleSheets) { - // if we haven't seen this style sheet, create it and pass a replaceSync with its contents - if (!styleSheet[styleSheetId]) { + const pageNum = metadataFields.pageNum; + // if we haven't seen this style sheet, create it and call replaceSync with its contents to bootstrap it + if (styleSheet[styleSheetPageNum] !== pageNum) { + styleSheet[styleSheetPageNum] = pageNum; styleSheet[styleSheetId] = shortid(); trackStyleChange(time(), styleSheet[styleSheetId], StyleSheetOperation.Create); trackStyleChange(time(), styleSheet[styleSheetId], StyleSheetOperation.ReplaceSync, getCssRules(styleSheet)); @@ -82,7 +93,6 @@ export function compute(): void { export function reset(): void { state = []; - } export function stop(): void { diff --git a/packages/clarity-js/src/layout/traverse.ts b/packages/clarity-js/src/layout/traverse.ts index 313e53a1..ebdf6ff3 100644 --- a/packages/clarity-js/src/layout/traverse.ts +++ b/packages/clarity-js/src/layout/traverse.ts @@ -4,41 +4,25 @@ import * as task from "@src/core/task"; import node from "@src/layout/node"; export default async function(root: Node, timer: Timer, source: Source): Promise { - let primaryQueue = [root]; - let slottedNodesQueue: Node[] = []; - for (var currentQueue of [primaryQueue, slottedNodesQueue]) { - while (currentQueue.length > 0) { - let entry = currentQueue.shift(); - let next = entry.firstChild; - - while (next) { - // During traversal there is not a guarantee that the assigned child here is being found after the slot to which it is placed - // as the typical parent/child methods don't reflect what is rendered. We need to make sure all other mutations and - // discoveries are processed before we process a slotted element. Once we are processing the slottedNodesQueue we can - // skip this effort and go directly to our breadth first traversal. - if (currentQueue == primaryQueue) { - var slottedParent = (next as HTMLElement).assignedSlot; - if (slottedParent) { - slottedNodesQueue.push(next); - } else { - currentQueue.push(next); - } - } else { - currentQueue.push(next); - } - next = next.nextSibling; - } - - // Check the status of current task to see if we should yield before continuing - let state = task.state(timer); - if (state === Task.Wait) { state = await task.suspend(timer); } - if (state === Task.Stop) { break; } - - // Check if processing a node gives us a pointer to one of its sub nodes for traversal - // E.g. an element node may give us a pointer to traverse shadowDom if shadowRoot property is set - // Or, an iframe from the same origin could give a pointer to it's document for traversing contents of iframe. - let subnode = node(entry, source); - if (subnode) { currentQueue.push(subnode); } + let queue = [root]; + while (queue.length > 0) { + let entry = queue.shift(); + let next = entry.firstChild; + + while (next) { + queue.push(next); + next = next.nextSibling; } + + // Check the status of current task to see if we should yield before continuing + let state = task.state(timer); + if (state === Task.Wait) { state = await task.suspend(timer); } + if (state === Task.Stop) { break; } + + // Check if processing a node gives us a pointer to one of its sub nodes for traversal + // E.g. an element node may give us a pointer to traverse shadowDom if shadowRoot property is set + // Or, an iframe from the same origin could give a pointer to it's document for traversing contents of iframe. + let subnode = node(entry, source); + if (subnode) { queue.push(subnode); } } -} +} \ No newline at end of file diff --git a/packages/clarity-js/types/data.d.ts b/packages/clarity-js/types/data.d.ts index 10f05ca8..f47b5378 100644 --- a/packages/clarity-js/types/data.d.ts +++ b/packages/clarity-js/types/data.d.ts @@ -109,7 +109,8 @@ export const enum Metric { MaxTouchPoints = 32, HardwareConcurrency = 33, DeviceMemory = 34, - Electron = 35 + Electron = 35, + ConstructedStyles = 36 } export const enum Dimension { diff --git a/packages/clarity-visualize/package.json b/packages/clarity-visualize/package.json index e949e408..1adb0d4f 100644 --- a/packages/clarity-visualize/package.json +++ b/packages/clarity-visualize/package.json @@ -1,6 +1,6 @@ { "name": "clarity-visualize", - "version": "0.7.25", + "version": "0.7.26", "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.7.25" + "clarity-decode": "^0.7.26" }, "devDependencies": { "@rollup/plugin-commonjs": "^24.0.0", diff --git a/packages/clarity-visualize/src/layout.ts b/packages/clarity-visualize/src/layout.ts index 33833e09..82b0bf10 100644 --- a/packages/clarity-visualize/src/layout.ts +++ b/packages/clarity-visualize/src/layout.ts @@ -17,7 +17,6 @@ export class LayoutHelper { animations = {}; state: PlaybackState = null; stylesToApply: { [id: string] : string[] } = {}; - styleSheetMap: { [id: number] : string[]; } = {}; constructor(state: PlaybackState) { this.state = state; @@ -115,18 +114,10 @@ export class LayoutHelper { } switch (event.data.operation) { case StyleSheetOperation.Create: - this.adoptedStyleSheets[event.data.id] = new CSSStyleSheet(); + this.adoptedStyleSheets[event.data.id] = new (this.state.window as any).CSSStyleSheet(); break; case StyleSheetOperation.Replace: styleSheet.replace(event.data.cssRules); - // Just changing the sheet isn't sufficient as we cannot rely on adoptedStyleSheets in visualiation - // when an underlying style sheet changes, we reset the styles on the element - for (var documentIdAsString of Object.keys(this.styleSheetMap)) { - var documentId = parseInt(documentIdAsString, 10); - if (this.styleSheetMap[documentId].indexOf(event.data.id as string) > -1) { - this.setDocumentStyles(documentId, this.styleSheetMap[documentId]); - } - } break; case StyleSheetOperation.ReplaceSync: styleSheet.replaceSync(event.data.cssRules); @@ -150,34 +141,14 @@ export class LayoutHelper { return; } - this.styleSheetMap[documentId] = styleIds; - let newSheets = styleIds.map(x => this.adoptedStyleSheets[x] as CSSStyleSheet); - - let styleNode = targetDocument.getElementById(Constant.AdoptedStyleSheet) ?? this.state.window.document.createElement("style"); - styleNode.id = Constant.AdoptedStyleSheet; - let ruleLengths = []; - styleNode.textContent = newSheets.map(x => { let newRule = this.getCssRules(x); ruleLengths.push(newRule.length); return newRule; }).join('\n'); - styleNode.setAttribute('data-parentid', `${documentId}`); - if (targetDocument.head) { - targetDocument.head.appendChild(styleNode); - } else { - targetDocument.appendChild(styleNode); - } - } - - private getCssRules(sheet: CSSStyleSheet): string { - let value = Constant.Empty as string; - let cssRules = null; - try { cssRules = sheet ? sheet.cssRules : []; } catch (e) { - if (e && e.name !== "SecurityError") { throw e; } - } - - if (cssRules !== null) { - for (let i = 0; i < cssRules.length; i++) { - value += cssRules[i].cssText; + let newSheets: CSSStyleSheet[] = []; + for (var styleId of styleIds) { + let styleSheet = this.adoptedStyleSheets[styleId]; + if (styleSheet) { + newSheets.push(styleSheet); } } - return value; + targetDocument.adoptedStyleSheets = newSheets } public exists = (hash: string): boolean => {