@palette.dev/browser
Version: 
Monitor your web app's performance
1,193 lines (1,178 loc) • 60.9 kB
JavaScript
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 handler = () => {
        start(opts);
        clearTimeout(_timeoutId);
    };