UNPKG

@palette.dev/browser

Version:

Monitor your web app's performance

1,193 lines (1,177 loc) 61.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const ENDPOINT = "https://api.palette.dev/api/collect"; const UNKNOWN = "<unknown>"; const isServerSide = typeof window === "undefined"; // The minimum length of a stringified payload to be compressed const GZIP_MIN_LEN = 1_000; const handlers = new Map(); const events = { emit(event, val) { const _handlers = handlers.get(event); // @ts-ignore if (_handlers) _handlers.forEach((h) => h(val)); }, on(event, cb) { const _handlers = handlers.get(event); if (_handlers) { if (true && _handlers.length > 10) { console.warn(`[palette/events] ${event} has more than 10 handlers`); } _handlers.push(cb); } else { handlers.set(event, [cb]); } }, off(event, cb) { const _handlers = handlers.get(event); if (_handlers) { if (!cb) { _handlers.length = 0; return; } const idx = _handlers.indexOf(cb); _handlers.splice(idx); } }, }; class PaletteError extends Error { constructor(message) { super(message + `\nThis error will be suppressed in production`); this.name = "PaletteError"; } } const onIdle = (cb) => { if ("scheduler" in globalThis) { // @ts-ignore return scheduler.postTask(cb, { priority: "background" }); } return typeof requestIdleCallback === "function" ? requestIdleCallback(cb) : setTimeout(cb, 0); }; /** * Logging utilities with [palette] prefix */ const PREFIX = "[palette]"; const logger = { log: (...args) => console.log(PREFIX, ...args), warn: (...args) => console.warn(PREFIX, ...args), error: (...args) => console.error(PREFIX, ...args), info: (...args) => console.info(PREFIX, ...args), debug: (...args) => console.debug(PREFIX, ...args), }; // We re-instance if the page was idle for more than 5 minutes. const RE_INSTANCE_TIMEOUT = 5 * 60 * 1_000; const randStr = () => Math.random().toString(36).substring(2); /** * Given the queue, prepare a payload and transport it. */ const flushHelper = async (queue, transport, beforeSend) => { const data = queue.clear(); data.forEach((datum) => { // Truncate before flush to save bandwidth datum.time = Math.trunc(datum.time); if (beforeSend) { const e = beforeSend({ type: datum.type, details: datum.details, }); if (e?.type) datum.type = e.type; if (e?.details) datum.details = e.details; } }); const payload = { data, // Format as a date to capture local timezone timeOrigin: new Date(performance.timeOrigin).toISOString(), tags: Object.fromEntries(tags), }; // In dev, // if debug mode, log payload to console // send "beacon" event to notify events are being sent // In prod, // send payload // @todo: attempt to retry failed requests try { const key = settings.get("key"); if (typeof key !== "string") throw new PaletteError("Key is required"); const isDebug = settings.get("debug"); const endpoint = settings.get("endpoint"); if (true && isDebug) logger.log(payload); await transport(endpoint, payload, { key, }); } catch (error) { if (true) logger.log("Failed to transport", error); return; } }; // A hack to get references working with withQueue let _queue = {}; const defaultOpts = { endpoint: ENDPOINT, plugins: [], debug: false, enabled: true, }; const getRegion = () => { const lang = navigator.language; if (!lang) return; const locale = new Intl.Locale(lang); return locale.region; }; const initHelper = (userAgent, transport, queue, flush, cb) => { _queue.q = queue; return (opts) => { // Disable palette on browsers which don't support modern web apis if (!performance.timeOrigin) return; const { endpoint, plugins, key, version, enabled: _enabled, debug: isDebug } = { ...defaultOpts, ...opts }; if (true) { if (!key) throw new PaletteError("Key required"); if (typeof key !== "string") throw new PaletteError("Key must be a string"); if (globalThis.__palette__) { // Warn of duplicate registration if (isDebug) logger.warn("Palette already initialized"); } } // Disable palette in production if key is not provided const enabled = key ? _enabled : false; // Populate settings settings.set("debug", isDebug); settings.set("key", key); settings.set("endpoint", endpoint); settings.set("enabled", enabled); // Exit early if palette is disabled // Only respect the enabled flag in production if (!true && enabled === false) return; // Only collect measures in prod. Performance markers are not supported (in node) if ("browser" === "browser" && !true) { performance.mark("palette.startInit"); } // Define the client versions const client = ["@palette.dev/browser", "1.30.0", "fd397c1"]; // Set global settings // Prevent SSR double-registering the "__palette__" // In dev, we prevent hot reload from re-initializing this global if (!globalThis.__palette__) { Object.defineProperty(globalThis, "__palette__", { value: { client: Object.freeze(client), endpoint, }, // We overwrite __palette__ in our tests writable: "development" === "test", }); } // Populate tags tag("palette.process", "browser"); tags.set("palette.client", client); // Set app version tag if (version) { tag("palette.app", version); } else if ("PALETTE_APP_VERSION" in globalThis) { tag("palette.app", PALETTE_APP_VERSION); } else if (true) { if (typeof __webpack_require__ === "function") { logger.warn(`Upload source maps to get the best experience using @palette.dev/webpack-plugin See https://palette.dev/docs/webpack-plugin `); } else { throw new PaletteError(`The "version" option is required when not using a bundler plugin. Please set it to the current git commit hash. See https://palette.dev/docs/versioning `); } } // Set session id tag("palette.sessionId", randStr()); // Set instanceId tag // Re-instancing breaks long-lived sessions into into smaller chunks. // We re-instance if the page was idle for more than 5 minutes. setInstance(); events.on("onAdd", debounce(setInstance, RE_INSTANCE_TIMEOUT)); events.on("onFull", () => flush(queue, transport, opts.beforeSend)); // Set PID tag if ("browser" === "browser") { // Main thread has 0 as PID // Worker processes should have non-zero PIDs tags.set("palette.pid", 0); } else if ("browser" === "electron.main" || "browser" === "node") { // Set a pid, used to bucket events tags.set("palette.pid", process.pid); } // Tag location in browser and electron renderer if ("browser" === "browser" && globalThis.location && !tags.has("palette.location")) { tag("palette.location", globalThis.location.toString()); } if ("browser" === "electron.renderer" && !tags.has("palette.location")) { const _location = globalThis.location.toString(); const delimiter = "app.asar"; // The paths preceding `app.asar` are machine-dependant, so we remove them. // If the app isn't packaged with asar, we return the full path. tag("palette.location", _location.includes(delimiter) ? _location.slice(_location.indexOf(delimiter) + delimiter.length) : _location); } // Tag region and connection type in browser and electron renderer if (("browser" === "browser" || "browser" === "electron.renderer") && typeof navigator !== "undefined") { const region = getRegion(); if (region) tag("palette.region", region); const conn = navigator.connection?.effectiveType; if (conn) tag("palette.connection", conn); } userAgent(queue).then(() => { // Callbacks can perform some validation before initialization below cb?.(tags, opts.beforeSend); }); // In dev, immediately emit "setup complete" event with env = dev and no data // @hack use computed property to avoid replacement. This hack allows checking the NODE_ENV value at runtime if (true && process.env["NODE_ENV"] !== "test") { transport(endpoint, { data: [] }, { key, env: "development", }).then((res) => { if (isDebug && res.ok) { logger.log("Palette - Successfully connected to the server"); } }); } if (isServerSide) return; if (true && !plugins.length) { logger.warn("Palette initialization requires at least one plugin and is a no-op otherwise"); } if ("browser" === "browser" || "browser" === "electron.renderer") { const callback = () => { plugins?.forEach((plugin) => { if (plugin) plugin(); }); }; if (document.prerendering) { addEventListener("prerenderingchange", () => callback(), true); } else { callback(); } } else { plugins?.forEach((plugin) => { if (plugin) plugin(); }); } if ("browser" === "browser" && !true) { performance.measure("palette.init", "palette.startInit"); } }; }; /** * Give a plugin access to the queue. * @returns The plugin, with the same arguments and return type */ const withQueue = (plugin) => { // Assign extra properties from E return Object.assign(plugin.bind(plugin, _queue), plugin); }; const checkInit = (name, val) => { if (!val) { const str = `The ${name} plugin wasn't initialized yet. Pass it to init() before using it`; throw new PaletteError(str); } }; const tags = new Map(); /** * Add a tag to each queue item. Tags are immutable to ensure consistency * * @param name - The name of the tag * @param value - The value of the tag * * @example * ```ts * import {tag} from 'palette.dev'; * * tag('user-id', 'u-123'); * ``` */ function tag(name, value) { if (true) { if (typeof name !== "string") throw new PaletteError("Tag name must be a string"); if (typeof value !== "string") throw new PaletteError("Tag value must be a string"); const isDebug = settings.get("debug"); if (isDebug) logger.log(`Tag: ${name} = ${value}`); } tags.set(name, value); } const setInstance = () => { const id = randStr(); tag("palette.instanceId", id); return id; }; const settings = new Map(); const isEnabled = () => settings.get("enabled") === true; const perfObserverIsSupported = () => typeof PerformanceObserver === "function"; /** * A utility for debouncing frequent events, making them easier to profile * @param cb - The function to debounce * @param beforeOrTimeout - A non-debounced function to call or the debounce timeout * @param timeout - The debounce timeout * @returns * * @example * ```ts * // Debounce `someFn` for 1 second * const debouncedFn = debounce(fn, 1_000); * * debouncedFn(); // fn called * debouncedFn(); // fn *not* called * setTimeout(() => { * debouncedFn(); // fn called * }, 1_000); * * // Debounce `someFn` for 1 second, log 'hello' every time `debouncedFn` is called * const debouncedFn = debounce(fn, () => console.log('hello'), 1_000); * debouncedFn(); // fn called, hello logged * debouncedFn(); // fn *not* called, hello logged * ``` */ const debounce = (cb, beforeOrTimeout, timeout = 1_000) => { let timeoutId; const _timeout = typeof beforeOrTimeout === "number" ? beforeOrTimeout : timeout; const before = typeof beforeOrTimeout === "function" ? beforeOrTimeout : () => { }; return (...args) => { // Timeout id's are objects in node and numbers in browsers so perform a "nullish" // check here instead of a type check if (timeoutId === undefined) { before(true); } else { before(false); clearTimeout(timeoutId); } timeoutId = setTimeout(() => { cb(...args); timeoutId = undefined; }, _timeout); }; }; // This module handles the dispatch events to the network // @ts-ignore missing CompressionStream types const supportsCompression = typeof CompressionStream === "function"; const compress = async (payload) => { const stream = new Blob([payload], { type: "application/json" }) .stream() // @ts-ignore .pipeThrough(new CompressionStream("gzip")); return new Response(stream).arrayBuffer(); }; const transport = async (endpoint, payload, params) => { if (isServerSide) return { ok: false }; const stringified = JSON.stringify(payload); const shouldCompress = supportsCompression && stringified.length > GZIP_MIN_LEN; const body = shouldCompress ? await compress(stringified) : stringified; const url = `${endpoint}?${new URLSearchParams({ ...params, ...(shouldCompress ? { encoding: "gzip" } : {}), })}`; if (!navigator.onLine) return { ok: false }; return fetch(url, { body, method: "POST", // We use the keepalive option to keep the connection open after the page has // unloaded. This is the modern alternative to the navigator.sendBeacon API. // See https://javascript.info/fetch-api#keepalive keepalive: true, // Set priority to low to avoid competing with the app's network bandwidth // @ts-ignore priority: "low", mode: "no-cors", headers: { "Content-Type": "application/json", ...(shouldCompress ? { "Content-Encoding": "gzip" } : {}), }, }); }; // Determine optimal time to flush const flush = (queue, _transport, beforeSend) => { if (queue.buffer.length && navigator.onLine) { flushHelper(queue, transport, beforeSend); } }; const FLUSH_TIMEOUT = 5_000; // The minimum size of the queue before it is flushed. // This ensures we don't flush too often. const MAX_QUEUE_SIZE = 1_000; // The queue buffer const buffer = []; const clear$1 = () => queue$4.buffer.splice(0, queue$4.buffer.length); const add = (item) => { if (true) { if (settings.get("debug")) { logger.log(item.type, item.time, item.value, item); } // Avoid adding circular references JSON.stringify(item); } // The current recommendation is to use PageVisibility and exclude entries that are created // after the first time in which the document becomes hidden // https://github.com/w3c/paint-timing/issues/40#issuecomment-597199462 const len = buffer.push(item); events.emit("onAdd", item); if (len >= MAX_QUEUE_SIZE) { if (true && settings.get("debug")) logger.info("queue full"); events.emit("onFull", queue$4); clear$1(); } }; const queue$4 = { clear: clear$1, add, buffer, }; let firstHiddenTime = -1; const initHiddenTime = () => { // If the document is hidden when this code runs, assume it was always // hidden and the page was loaded in the background, with the one exception // that visibility state is always 'hidden' during prerendering, so we have // to ignore that case until prerendering finishes (see: `prerenderingchange` // event logic below). return document.visibilityState === "hidden" && !document.prerendering ? 0 : Infinity; }; const onVisibilityUpdate = (event) => { // If the document is 'hidden' and no previous hidden timestamp has been // set, update it based on the current event data. if (document.visibilityState === "visible") { firstHiddenTime = Infinity; } else if (document.visibilityState === "hidden" && firstHiddenTime > -1) { // If the event is a 'visibilitychange' event, it means the page was // visible prior to this change, so the event timestamp is the first // hidden time. // However, if the event is not a 'visibilitychange' event, then it must // be a 'prerenderingchange' event, and the fact that the document is // still 'hidden' from the above check means the tab was activated // in a background state and so has always been hidden. firstHiddenTime = event.type === "visibilitychange" ? event.timeStamp : 0; } }; const addChangeListeners = () => { // Report all available metrics whenever the page is backgrounded or unloaded. // Learn more here https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden // // In electron, visibility changes on show, hide, maximize, minimize, and restore events // See https://github.com/electron/electron/blob/ff13fa8f0a9ea589830ad2868011f510e652fb16/lib/browser/api/browser-window.ts#L42-L45 // // Unfortunately, the visibilitychange isn't very reliable in electron. Setting capture to false correctly triggers visiblitychange events // https://github.com/electron/electron/issues/28677 addEventListener("visibilitychange", onVisibilityUpdate, "browser" === "browser"); // IMPORTANT: when a page is prerendering, its `visibilityState` is // 'hidden', so in order to account for cases where this module checks for // visibility during prerendering, an additional check after prerendering // completes is also required. addEventListener("prerenderingchange", onVisibilityUpdate, true); }; const onBFCacheRestore = (cb) => { addEventListener("pageshow", (event) => { if (event.persisted) cb(event); }, true); }; const getVisibilityWatcher = () => { if (firstHiddenTime < 0) { // If the document is hidden when this code runs, assume it was hidden // since navigation start. This isn't a perfect heuristic, but it's the // best we can do until an API is available to support querying past // visibilityState. firstHiddenTime = initHiddenTime(); addChangeListeners(); // Reset the time on bfcache restores. onBFCacheRestore(() => { // Schedule a task in order to track the `visibilityState` once it's // had an opportunity to change to visible in all browsers. // https://bugs.chromium.org/p/chromium/issues/detail?id=1133363 setTimeout(() => { firstHiddenTime = initHiddenTime(); addChangeListeners(); }, 0); }); } return { get firstHiddenTime() { return firstHiddenTime; }, }; }; const onHidden = (cb) => { const onHiddenOrPageHide = (event) => { if (event.type === "pagehide" || document.visibilityState === "hidden") { cb(event); } }; addEventListener("visibilitychange", onHiddenOrPageHide, "browser" === "browser"); // Some browsers have buggy implementations of visibilitychange, // so we use pagehide in addition, just to be safe. addEventListener("pagehide", onHiddenOrPageHide, true); }; /** * Only flush the queue under certain conditions */ const init$8 = (queue, flush, transport, beforeSend) => { if (isServerSide) return; // Debounce adding to the queue. Once ready, flush when idle // Fallback to setTimeout(fn, 0) in the browser const lazyFlush = () => onIdle(() => flush(queue, transport, beforeSend)); events.on("onAdd", debounce(lazyFlush, FLUSH_TIMEOUT)); onHidden(lazyFlush); }; const whenActivated = (callback) => { if (document.prerendering) { addEventListener("prerenderingchange", () => callback(), true); } else { callback(); } }; const getGpuRenderer = () => { // Prevent WEBGL_debug_renderer_info deprecation warnings in firefox if (!window.chrome) return ""; const gl = document .createElement("canvas") // Get the specs for the fastest GPU available. This helps provide a better // picture of the device's capabilities. .getContext("webgl", { powerPreference: "high-performance" }); if (!gl) return ""; const ext = gl.getExtension("WEBGL_debug_renderer_info"); return ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : ""; }; let gpuModel; const init$7 = async () => { if (isServerSide) return; tags.set("palette.userAgent", { "cpu.cores": navigator.hardwareConcurrency, "gpu.model": gpuModel ?? UNKNOWN, // @ts-ignore `navigator.deviceMemory` is an experimental API so no type // defs exist just yet "memory.total": navigator.deviceMemory ?? UNKNOWN, // in GB }); // Lazily get the GPU model since getContext is expensive if ("browser" === "electron.renderer") { onIdle(() => (gpuModel = getGpuRenderer())); } }; var e,n,t,i,r,a=-1,o=function(e){addEventListener("pageshow",(function(n){n.persisted&&(a=n.timeStamp,e(n));}),!0);},c=function(){return window.performance&&performance.getEntriesByType&&performance.getEntriesByType("navigation")[0]},u=function(){var e=c();return e&&e.activationStart||0},f=function(e,n){var t=c(),i="navigate";a>=0?i="back-forward-cache":t&&(document.prerendering||u()>0?i="prerender":document.wasDiscarded?i="restore":t.type&&(i=t.type.replace(/_/g,"-")));return {name:e,value:void 0===n?-1:n,rating:"good",delta:0,entries:[],id:"v3-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12),navigationType:i}},s=function(e,n,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){var i=new PerformanceObserver((function(e){Promise.resolve().then((function(){n(e.getEntries());}));}));return i.observe(Object.assign({type:e,buffered:!0},t||{})),i}}catch(e){}},d=function(e,n,t,i){var r,a;return function(o){n.value>=0&&(o||i)&&((a=n.value-(r||0))||void 0===r)&&(r=n.value,n.delta=a,n.rating=function(e,n){return e>n[1]?"poor":e>n[0]?"needs-improvement":"good"}(n.value,t),e(n));}},l=function(e){requestAnimationFrame((function(){return requestAnimationFrame((function(){return e()}))}));},p=function(e){var n=function(n){"pagehide"!==n.type&&"hidden"!==document.visibilityState||e(n);};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0);},v=function(e){var n=!1;return function(t){n||(e(t),n=!0);}},m=-1,h=function(){return "hidden"!==document.visibilityState||document.prerendering?1/0:0},g$1=function(e){"hidden"===document.visibilityState&&m>-1&&(m="visibilitychange"===e.type?e.timeStamp:0,T());},y=function(){addEventListener("visibilitychange",g$1,!0),addEventListener("prerenderingchange",g$1,!0);},T=function(){removeEventListener("visibilitychange",g$1,!0),removeEventListener("prerenderingchange",g$1,!0);},E=function(){return m<0&&(m=h(),y(),o((function(){setTimeout((function(){m=h(),y();}),0);}))),{get firstHiddenTime(){return m}}},C=function(e){document.prerendering?addEventListener("prerenderingchange",(function(){return e()}),!0):e();},L=[1800,3e3],w=function(e,n){n=n||{},C((function(){var t,i=E(),r=f("FCP"),a=s("paint",(function(e){e.forEach((function(e){"first-contentful-paint"===e.name&&(a.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=Math.max(e.startTime-u(),0),r.entries.push(e),t(!0)));}));}));a&&(t=d(e,r,L,n.reportAllChanges),o((function(i){r=f("FCP"),t=d(e,r,L,n.reportAllChanges),l((function(){r.value=performance.now()-i.timeStamp,t(!0);}));})));}));},b=[.1,.25],S=function(e,n){n=n||{},w(v((function(){var t,i=f("CLS",0),r=0,a=[],c=function(e){e.forEach((function(e){if(!e.hadRecentInput){var n=a[0],t=a[a.length-1];r&&e.startTime-t.startTime<1e3&&e.startTime-n.startTime<5e3?(r+=e.value,a.push(e)):(r=e.value,a=[e]);}})),r>i.value&&(i.value=r,i.entries=a,t());},u=s("layout-shift",c);u&&(t=d(e,i,b,n.reportAllChanges),p((function(){c(u.takeRecords()),t(!0);})),o((function(){r=0,i=f("CLS",0),t=d(e,i,b,n.reportAllChanges),l((function(){return t()}));})),setTimeout(t,0));})));},A={passive:!0,capture:!0},I=new Date,P=function(i,r){e||(e=r,n=i,t=new Date,k(removeEventListener),F());},F=function(){if(n>=0&&n<t-I){var r={entryType:"first-input",name:e.type,target:e.target,cancelable:e.cancelable,startTime:e.timeStamp,processingStart:e.timeStamp+n};i.forEach((function(e){e(r);})),i=[];}},M=function(e){if(e.cancelable){var n=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,n){var t=function(){P(e,n),r();},i=function(){r();},r=function(){removeEventListener("pointerup",t,A),removeEventListener("pointercancel",i,A);};addEventListener("pointerup",t,A),addEventListener("pointercancel",i,A);}(n,e):P(n,e);}},k=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(n){return e(n,M,A)}));},D=[100,300],x=function(t,r){r=r||{},C((function(){var a,c=E(),u=f("FID"),l=function(e){e.startTime<c.firstHiddenTime&&(u.value=e.processingStart-e.startTime,u.entries.push(e),a(!0));},m=function(e){e.forEach(l);},h=s("first-input",m);a=d(t,u,D,r.reportAllChanges),h&&p(v((function(){m(h.takeRecords()),h.disconnect();}))),h&&o((function(){var o;u=f("FID"),a=d(t,u,D,r.reportAllChanges),i=[],n=-1,e=null,k(addEventListener),o=l,i.push(o),F();}));}));},B=0,R=1/0,H=0,N=function(e){e.forEach((function(e){e.interactionId&&(R=Math.min(R,e.interactionId),H=Math.max(H,e.interactionId),B=H?(H-R)/7+1:0);}));},O=function(){return r?B:performance.interactionCount||0},q=function(){"interactionCount"in performance||r||(r=s("event",N,{type:"event",buffered:!0,durationThreshold:0}));},j=[200,500],_=0,z=function(){return O()-_},G=[],J={},K=function(e){var n=G[G.length-1],t=J[e.interactionId];if(t||G.length<10||e.duration>n.latency){if(t)t.entries.push(e),t.latency=Math.max(t.latency,e.duration);else {var i={id:e.interactionId,latency:e.duration,entries:[e]};J[i.id]=i,G.push(i);}G.sort((function(e,n){return n.latency-e.latency})),G.splice(10).forEach((function(e){delete J[e.id];}));}},Q=function(e,n){n=n||{},C((function(){var t;q();var i,r=f("INP"),a=function(e){e.forEach((function(e){(e.interactionId&&K(e),"first-input"===e.entryType)&&(!G.some((function(n){return n.entries.some((function(n){return e.duration===n.duration&&e.startTime===n.startTime}))}))&&K(e));}));var n,t=(n=Math.min(G.length-1,Math.floor(z()/50)),G[n]);t&&t.latency!==r.value&&(r.value=t.latency,r.entries=t.entries,i());},c=s("event",a,{durationThreshold:null!==(t=n.durationThreshold)&&void 0!==t?t:40});i=d(e,r,j,n.reportAllChanges),c&&("PerformanceEventTiming"in window&&"interactionId"in PerformanceEventTiming.prototype&&c.observe({type:"first-input",buffered:!0}),p((function(){a(c.takeRecords()),r.value<0&&z()>0&&(r.value=0,r.entries=[]),i(!0);})),o((function(){G=[],_=O(),r=f("INP"),i=d(e,r,j,n.reportAllChanges);})));}));},U=[2500,4e3],V={},W=function(e,n){n=n||{},C((function(){var t,i=E(),r=f("LCP"),a=function(e){var n=e[e.length-1];n&&n.startTime<i.firstHiddenTime&&(r.value=Math.max(n.startTime-u(),0),r.entries=[n],t());},c=s("largest-contentful-paint",a);if(c){t=d(e,r,U,n.reportAllChanges);var m=v((function(){V[r.id]||(a(c.takeRecords()),c.disconnect(),V[r.id]=!0,t(!0));}));["keydown","click"].forEach((function(e){addEventListener(e,(function(){return setTimeout(m,0)}),!0);})),p(m),o((function(i){r=f("LCP"),t=d(e,r,U,n.reportAllChanges),l((function(){r.value=performance.now()-i.timeStamp,V[r.id]=!0,t(!0);}));}));}}));},X=[800,1800],Y=function e(n){document.prerendering?C((function(){return e(n)})):"complete"!==document.readyState?addEventListener("load",(function(){return e(n)}),!0):setTimeout(n,0);},Z=function(e,n){n=n||{};var t=f("TTFB"),i=d(e,t,X,n.reportAllChanges);Y((function(){var r=c();if(r){var a=r.responseStart;if(a<=0||a>performance.now())return;t.value=Math.max(a-u(),0),t.entries=[r],i(!0),o((function(){t=f("TTFB",0),(i=d(e,t,X,n.reportAllChanges))(!0);}));}}));}; // @ts-ignore let queue$3; const addToQueue = (metric) => { const { name, value } = metric; queue$3.add({ type: `vitals.${name.toLowerCase()}`, time: metric.entries[0]?.startTime ?? 0, value, details: metric.entries.map((e) => e.toJSON?.()), }); }; const init$6 = withQueue(({ q }) => { if (isServerSide) return; queue$3 = q; // @hack: LayoutShiftAttribution breaks in electron when transported to main process so disabled for now if ("browser" === "browser") S(addToQueue, { reportAllChanges: true }); w(addToQueue); W(addToQueue); Z(addToQueue); x(addToQueue); Q(addToQueue, { reportAllChanges: true }); }); const transactions = { names: [], timestamps: [], length: 0, }; let countInProgress = 0; // start a transaction and return its id // transaction ids are used to cancel transactions const start$1 = (timestamp, event) => { countInProgress++; if (true) { transactions.names.push(event); transactions.timestamps.push(Math.trunc(timestamp)); } events.emit(event); if (true && settings.get("debug")) logger.log("transaction start", event); return transactions.length++; }; const isIdle = () => countInProgress === 0; const clear = () => { transactions.length = 0; transactions.timestamps.length = 0; transactions.names.length = 0; countInProgress = 0; }; // finish a transaction and add it to the queue const end = (id, value, details) => { countInProgress--; const start = transactions.timestamps[id]; if (true && settings.get("debug")) { logger.log("transaction end", transactions.names[id], value, details); } // debounce stopping the profiler after 1s of transaction inactivity if (isIdle()) { clear(); if (true && settings.get("debug")) logger.log("idle"); events.emit("idle"); } // eventually we want to do something like this to avoid extra array allocation: // queue.start.push(start) // queue.value.push(value) // queue.details.push(details) if (true) { return [start, value, details]; } }; // finish a transaction without adding it to the queue const cancel = (id) => { countInProgress--; if (true && settings.get("debug")) { logger.log("transaction cancel", transactions.names[id]); } if (isIdle()) { clear(); if (true && settings.get("debug")) logger.log("idle"); events.emit("idle"); } }; const _measure = "markers.measure"; const _mark = "markers.mark"; // @todo: move this to events plugin const _longtask = "events.longtask"; const init$5 = withQueue(({ q }) => { // @hack In dev, react adds timings for each component render to the performance timeline. This // overwhelms queue.add() and degrades performance in dev. As a workaround, we disable the // measure plugin in dev. if (true || isServerSide || !perfObserverIsSupported()) return; whenActivated(() => { const watcher = getVisibilityWatcher(); const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); const { firstHiddenTime } = watcher; entries.forEach(({ startTime, duration, name, entryType, detail }) => { if (name.startsWith("GTM-") || name.includes("grammarly")) return; const type = entryType === "mark" ? _mark : entryType === "measure" ? _measure : _longtask; if (startTime < firstHiddenTime) { q.add({ type, time: startTime, value: entryType === "mark" ? startTime : duration, details: detail ? { name, detail } : { name }, }); } }); }); const entryTypes = ["mark", "measure"]; if (PerformanceObserver.supportedEntryTypes.includes("longtask")) { entryTypes.push("longtask"); } entryTypes.forEach((entryType) => { observer.observe({ type: entryType, // Buffering allows us to capture long tasks that happen before the observer is created. buffered: true, }); }); }); }); const measure = { start: (name) => { if (true && settings.get("debug")) logger.log("measure.start", name); const { startTime } = performance.mark(name); start$1(startTime, _measure); }, end: (name) => { if (true && settings.get("debug")) logger.log("measure.end", name); try { performance.measure(name, name); } catch (e) { if (true) throw new PaletteError(`Failed to execute 'measure.end': 'measure.start' has not been called with '${name}'.`); } cancel(0); }, }; let queue$2; /** * Given a child DOM element, returns a query-selector statement describing that * and its ancestors * e.g. [HTMLElement] => body > div > input#foo.btn[name=baz] * @returns generated DOM path */ function htmlTreeAsString(elem, keyAttrs) { // try/catch both: // - accessing event.target (see getsentry/raven-js#838, #768) // - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly // - can throw an exception in some circumstances. try { let currentElem = elem; const MAX_TRAVERSE_HEIGHT = 5; const MAX_OUTPUT_LEN = 80; const out = []; let height = 0; let len = 0; const separator = " > "; const sepLength = separator.length; let nextStr; // eslint-disable-next-line no-plusplus while (currentElem && height++ < MAX_TRAVERSE_HEIGHT) { nextStr = _htmlElementAsString(currentElem, keyAttrs); // bail out if // - nextStr is the 'html' element // - the length of the string that would be created exceeds MAX_OUTPUT_LEN // (ignore this limit if we are on the first iteration) if (nextStr === "html" || (height > 1 && len + out.length * sepLength + nextStr.length >= MAX_OUTPUT_LEN)) { break; } out.push(nextStr); len += nextStr.length; currentElem = currentElem.parentNode; } return out.reverse().join(separator); } catch (_oO) { return UNKNOWN; } } /** * Returns a simple, query-selector representation of a DOM element * e.g. [HTMLElement] => input#foo.btn[name=baz] * @returns generated DOM path */ function _htmlElementAsString(el, keyAttrs) { const elem = el; const out = []; let className; let classes; let key; let attr; let i; if (!elem || !elem.tagName) { return ""; } out.push(elem.tagName.toLowerCase()); // Pairs of attribute keys defined in `serializeAttribute` and their values on element. const keyAttrPairs = keyAttrs && keyAttrs.length ? keyAttrs.filter((keyAttr) => elem.getAttribute(keyAttr)).map((keyAttr) => [keyAttr, elem.getAttribute(keyAttr)]) : null; if (keyAttrPairs && keyAttrPairs.length) { keyAttrPairs.forEach((keyAttrPair) => { out.push(`[${keyAttrPair[0]}="${keyAttrPair[1]}"]`); }); } else { if (elem.id) { out.push(`#${elem.id}`); } // eslint-disable-next-line prefer-const className = elem.className; if (className && typeof className === "string") { classes = className.split(/\s+/); for (i = 0; i < classes.length; i++) { out.push(`.${classes[i]}`); } } } const allowedAttrs = ["type", "name", "title", "alt"]; for (i = 0; i < allowedAttrs.length; i++) { key = allowedAttrs[i]; attr = elem.getAttribute(key); if (attr) { out.push(`[${key}="${attr}"]`); } } return out.join(); } /** * Decide whether an event should be captured. * @param event event to be captured */ function shouldSkipDOMEvent(event) { // We are only interested in filtering `keypress` events for now. if (event.type !== "keypress") { return false; } try { const target = event.target; if (!target || !target.tagName) { return true; } // Only consider keypress events on actual input elements. This will disregard keypresses targeting body // e.g.tabbing through elements, hotkeys, etc. if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) { return false; } } catch { // just accessing `target` property can throw an exception in some rare circumstances // see: https://github.com/getsentry/sentry-javascript/issues/838 } return true; } /** * Decide whether the current event should finish the debounce of previously captured one. * @param previous previously captured event * @param current event to be captured */ function shouldShortcircuitPreviousDebounce(previous, current) { // If there was no previous event, it should always be swapped for the new one. if (!previous) { return true; } // If both events have different type, then user definitely performed two separate actions. e.g. click + keypress. if (previous.type !== current.type) { return true; } try { // If both events have the same type, it's still possible that actions were performed on different targets. // e.g. 2 clicks on different buttons. if (previous.target !== current.target) { return true; } } catch { // just accessing `target` property can throw an exception in some rare circumstances // see: https://github.com/getsentry/sentry-javascript/issues/838 } // If both events have the same type _and_ same `target` (an element which triggered an event, _not necessarily_ // to which an event listener was attached), we treat them as the same action, as we want to capture // only one event. e.g. multiple clicks on the same button, or typing inside a user input box. return false; } const debounceDuration = 10; let debounceTimeoutID; let lastCapturedEvent; /** * Wraps addEventListener to capture UI events * @param handler function that will be triggered * @param globalListener indicates whether event was captured by the global event listener * @returns wrapped event handler * @hidden */ function makeDOMEventHandler(handler, globalListener = false) { return (event) => { // It's possible this handler might trigger multiple times for the same // event (e.g. event propagation through node ancestors). // Ignore if we've already captured that event. if (!event || lastCapturedEvent === event) { return; } // We always want to skip _some_ events. if (shouldSkipDOMEvent(event)) { return; } const name = event.type === "keypress" ? "input" : event.type; // If there is no debounce timer, it means that we can safely capture the new event and store it for future comparisons. if (debounceTimeoutID === undefined) { handler({ event, name, global: globalListener, }); lastCapturedEvent = event; } // If there is a debounce awaiting, see if the new event is different enough to treat it as a unique one. // If that's the case, emit the previous event and store locally the newly-captured DOM event. else if (shouldShortcircuitPreviousDebounce(lastCapturedEvent, event)) { handler({ event, name, global: globalListener, }); lastCapturedEvent = event; } // Start a new debounce timer that will prevent us from capturing multiple events that should be grouped together. clearTimeout(debounceTimeoutID); debounceTimeoutID = window.setTimeout(() => { debounceTimeoutID = undefined; }, debounceDuration); }; } const listenerEvents = []; // Track which elements have Palette listeners registered per event type const elementEventMap = new WeakMap(); let patchedSearchParams = false; let patchedEventListener = false; function patchAddEventListener() { if (patchedEventListener) return; patchedEventListener = true; const originalAddEventListener = EventTarget.prototype.addEventListener; const originalRemoveEventListener = EventTarget.prototype.removeEventListener; EventTarget.prototype.addEventListener = function patchedAddEventListener(_type, listener, options) { // Check if this is an event type we're monitoring const paletteEvent = listenerEvents.find((event) => event.type === _type); if (paletteEvent && this && typeof listener === "function") { // Track if we've already added Palette's listener for this event type on this element let eventTypes = elementEventMap.get(this); if (!eventTypes) { eventTypes = new Set(); elementEventMap.set(this, eventTypes); } // Only add Palette's listener once per element per event type if (!eventTypes.has(_type)) { eventTypes.add(_type); // Register Palette's listener FIRST with capture to ensure it runs before user's listener originalAddEventListener.call(this, _type, paletteEvent.listener, { capture: true }); } } // Always register the user's original listener return originalAddEventListener.call(this, _type, listener, options); }; // Keep original removeEventListener - cleanup is complex and elements will be garbage collected anyway EventTarget.prototype.removeEventListener = originalRemoveEventListener; } /** * A HOC that creates a function that creates events from DOM API calls. * This is a HOC so that we get access to dom options in the closure. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function addDOMEvent(handlerData) { let target; // @ts-ignore let keyAttrs = undefined; // Accessing event.target can throw (see getsentry/raven-js#838, #768) try { target = handlerData.event.target ? htmlTreeAsString(handlerData.event.target, keyAttrs) : htmlTreeAsString(handlerData.event, keyAttrs); } catch { target = UNKNOWN; } if (target.length === 0) { return; } queue$2.add({ type: `events.${handlerData.event.type}`, time: handlerData.event.timeStamp, details: { target, }, }); } // Debounce starting the profiler const debounceEvent = (type) => { let lastFiredAt; let startedAt; return debounce(() => { queue$2.add({ type, time: startedAt, value: lastFiredAt - startedAt, }); }, (fistStart) => { if (fistStart) startedAt = performance.now(); lastFiredAt = performance.now(); }); }; /** * Track DOM events like clicks, scroll, and keypress events */ const init$4 = withQueue(({ q }) => { if (isServerSide) return; queue$2 = q; const globalDOMEventHandler = makeDOMEventHandler(addDOMEvent, true); addEventListener("click", globalDOMEventHandler, true); addEventListener("wheel", debounceEvent("events.scroll"), true); addEventListener("mousemove", debounceEvent("events.mousemove"), true); addEventListener("keypress", debounceEvent("events.keypress"), true); const { readyState } = document; const opts = { once: true, capture: true, }; // 'DOMContentLoaded' is fired when the browser fully loaded HTML, and the DOM tree is built, // but external resources like pictures <img> and stylesheets may not yet have loaded. // // When readyState is 'loading', DCL has not fired yet if (readyState === "loading") { const type = "events.dcl"; const id = start$1(0, type); addEventListener("DOMContentLoaded", (e) => { end(id, e.timeStamp); q.add({ type, time: 0, value: e.timeStamp, }); }, // Avoid triggering from soft-nav opts); } if (readyState !== "complete") { const type = "events.load"; const id = start$1(0, type); // 'load' is fired when external resources are loaded, so styles are applied, image sizes are known etc. // When readyState is 'compelete', DL has already fired addEventListener("load", (e) => { end(id, e.timeStamp); q.add({ type, time: 0, value: e.timeStamp, }); }, // Avoid triggering from soft-nav opts); } function onSearchParamsChange() { if (patchedSearchParams) return; patchedSearchParams = true; const tagSearchParams = () => { try { const params = new URLSearchParams(location.search); params.forEach((value, key) => { tag(`router.query.${key}`, value); }); } catch { // noop } }; const dispatch = () => { // One event you can listen to everywhere window.dispatchEvent(new Event("palette.urlchange")); }; // Patch pushState/replaceState so programmatic navigations emit ["pushState", "replaceState"].forEach((method) => { // @ts-ignore const original = history[method]; // @ts-ignore history[method] = function (...args) { const prev = location.href; const ret = original.apply(this, args); // Only dispatch if the URL actually changed if (location.href !== prev) dispatch(); return ret; }; }); window.addEventListener("palette.urlchange", () => { tag("palette.location", location.pathname); tagSearchParams(); }); // Back/forward navigations window.addEventListener("popstate", () => { // tag all search params tag("palette.location", location.pathname); tagSearchParams(); }); // Initial tagging on load tag("palette.location", location.pathname); tagSearchParams(); } onSearchParamsChange(); }); let queue$1; const isProfilerSupported = typeof Profiler !== "undefined"; const profiler = ({ q }) => { queue$1 = q; if (true && !isProfilerSupported && settings.get("debug")) { logger.log("Profiler unsupported in this browser"); } }; let _profiler; let startedTime; const start = (opts) => { if (true) { if (settings.get("debug")) logger.log("profiler.start"); checkInit("profiler", queue$1); } else if (isProfilerSupported && (!_profiler || _profiler.stopped) && isEnabled()) { try { _profiler = new Profiler(opts); startedTime = performance.now(); _profiler.addEventListener("samplebufferfull", stop); } catch (e) { if (true) logger.log(e); } } }; const stop = async () => { if (true) { if (settings.get("debug")) logger.log("profiler.stop"); checkInit("profiler", queue$1); } else if (!isEnabled() || !_profiler || _profiler.stopped) return; else { const endTime = performance.now(); const profile = await _profiler.stop(); queue$1.add({ type: "profile", time: startedTime, value: endTime - startedTime, // @ts-ignore profiler index signature details: profile, }); } }; let _timeoutId; const stopOnIdle = () => { clearTimeout(_timeoutId); _timeoutId = globalThis.setTimeout(() => { stop(); _timeoutId = undefined; }, 1_000); }; const on = (events$1, opts) => { // if (!isEnabled() || !isProfilerSupported) return; // if (isListening) { // if (true) // console.warn( // "Profiler is already listening to events from a previous call to '.on'. Ignoring call to profiler.on" // ); // return; // } // isListening = true; const hand