UNPKG

clarity-js

Version:

An analytics library that uses web page interactions to generate aggregated insights

142 lines (133 loc) 6.31 kB
import { Code, Constant, Dimension, Metric, PerformanceEventTiming, Severity } from "@clarity-types/data"; import { FunctionNames } from "@clarity-types/performance"; import config from "@src/core/config"; import { bind } from "@src/core/event"; import measure from "@src/core/measure"; import { setTimeout } from "@src/core/timeout"; import * as dimension from "@src/data/dimension"; import * as metric from "@src/data/metric"; import * as internal from "@src/diagnostic/internal"; import * as interaction from "@src/performance/interaction"; import * as navigation from "@src/performance/navigation"; let observer: PerformanceObserver; const types: string[] = [ Constant.Navigation, Constant.Resource, Constant.LongTask, Constant.FID, Constant.CLS, Constant.LCP, Constant.PerformanceEventTiming, ]; export function start(): void { // Capture connection properties, if available if (navigator && "connection" in navigator) { // biome-ignore lint/complexity/useLiteralKeys: connection isn't on every browser, so effectiveType gives unncessary typescript error dimension.log(Dimension.ConnectionType, navigator.connection["effectiveType"]); } // Check the browser support performance observer as a pre-requisite for any performance measurement if (window.PerformanceObserver && PerformanceObserver.supportedEntryTypes) { // Start monitoring performance data after page has finished loading. // If the document.readyState is not yet complete, we intentionally call observe using a setTimeout. // This allows us to capture loadEventEnd on navigation timeline. if (document.readyState !== "complete") { bind(window, "load", setTimeout.bind(this, observe, 0)); } else { observe(); } } else { internal.log(Code.PerformanceObserver, Severity.Info); } } function observe(): void { observe.dn = FunctionNames.ObserverObserve; // Some browsers will throw an error for unsupported entryType, e.g. "layout-shift" // In those cases, we log it as a warning and continue with rest of the Clarity processing try { if (observer) { observer.disconnect(); } observer = new PerformanceObserver(measure(handle) as PerformanceObserverCallback); // Reference: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver/observe // "buffered" flag indicates whether buffered entries should be queued into the observer's buffer. // 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 (const x of types) { if (PerformanceObserver.supportedEntryTypes.indexOf(x) >= 0) { // 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); } observer.observe({ type: x, buffered: true }); } } } catch { internal.log(Code.PerformanceObserver, Severity.Warning); } } function handle(entries: PerformanceObserverEntryList): void { handle.dn = FunctionNames.ObserverHandle; process(entries.getEntries()); } function process(entries: PerformanceEntryList): void { const visible = "visibilityState" in document ? document.visibilityState === "visible" : true; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; switch (entry.entryType) { case Constant.Navigation: navigation.compute(entry as PerformanceNavigationTiming); break; case Constant.Resource: { const name = entry.name; dimension.log(Dimension.NetworkHosts, host(name)); if (name === config.upload || name === config.fallback) { metric.max(Metric.UploadTime, entry.duration); } break; } case Constant.LongTask: metric.count(Metric.LongTaskCount); break; case Constant.FID: // biome-ignore lint/suspicious/noExplicitAny: not all browsers support processingstart if (visible && (entry as any).processingStart) { // biome-ignore lint/suspicious/noExplicitAny: not all browsers support processingstart metric.max(Metric.FirstInputDelay, (entry as any).processingStart - entry.startTime); } break; case Constant.PerformanceEventTiming: if (visible && "PerformanceEventTiming" in window && "interactionId" in PerformanceEventTiming.prototype) { 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()); } break; case Constant.CLS: // Scale the value to avoid sending back floating point number // biome-ignore lint/suspicious/noExplicitAny: not all browsers support hadRecentInput if (visible && !(entry as any).hadRecentInput) { // biome-ignore lint/suspicious/noExplicitAny: not all browsers support layoutshift value metric.sum(Metric.CumulativeLayoutShift, (entry as any).value * 1000); } break; case Constant.LCP: if (visible) { metric.max(Metric.LargestPaint, entry.startTime); } break; } } } export function stop(): void { if (observer) { observer.disconnect(); } observer = null; interaction.resetInteractions(); } function host(url: string): string { const a = document.createElement("a"); a.href = url; return a.host; }