@palette.dev/browser
Version:
Monitor your web app's performance
1,193 lines (1,177 loc) • 61.1 kB
JavaScript
'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