Site Measurements

Logs fetch, xhr, ws requests in console

2024-08-07 기준 버전입니다. 최신 버전을 확인하세요.

// ==UserScript==
// @name         Site Measurements
// @namespace    http://tampermonkey.net/
// @version      2024-08-07.4
// @license      MIT
// @description  Logs fetch, xhr, ws requests in console
// @author       Grosmar
// @match        https://*.livejasmin.com/*
// @match        https://livejasmin.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=livejasmin.com
// @grant        none
// @run-at document-start
// ==/UserScript==

(function() {
    'use strict';
    // Measurement prep
    let measurements = [];
    const pushMeasurement = (msg) => { if (msg) { measurements.push( {time: Date.now(), msg: msg } ) } return msg; };


    // CONFIG
    let networkLogsEnabled = localStorage.getItem("networkMeasuermentConsoleLogs") == "true";
    const filteredWs = /jaws\./; // these websockets will be filtered out from logging
    const binaryWsPackageCountLimit = 3; //show only this amount of binary packages
    const wsEventFilter = { r: /onRandomAccessPoint|onMetaData/, limit: 2} // messages with this content will be shown only `limit` amount of time
    const injectWsSend = { pattern: /"join",\{(?!.*enableTrace)/, replacement: '"join",{"enableTrace":true,' }; // if message pattern matches, injects this into websocket command

    let wsBinaryCounter = 0; // Used to determine second binary package for keyframe


    const measurementConfig =
          [
              {
                  callType: /^(?:NETWORK_User_MouseUp|NETWORK_User_TouchStart)/,
                  filter: null,
                  filterArgIndex: null,
                  func: (args) => "USER_START"
              },
              {
                  callType: /^NETWORK_Ws_Send/,
                  filter: /"join"/,
                  filterArgIndex: 1,
                  func: (args) => "OC_JOIN" //measurements = measurements.concat(JSON.parse(args[0].substr(3)).map( e =>
              },
              {
                  callType: /^NETWORK_Ws_Msg/,
                  filter: /"getEdge"/,
                  filterArgIndex: 1,
                  func: (args) => "OC_JOINED" //measurements = measurements.concat(JSON.parse(args[0].substr(3)).map( e =>
              },
              {
                  callType: /^NETWORK_Ws_Msg/,
                  filter: /setVideoData/,
                  filterArgIndex: 1,
                  func: (args) => "OC_SET_VIDEO_DATA" //measurements = measurements.concat(JSON.parse(args[0].substr(3)).map( e =>
              },
              {
                  callType: /^NETWORK_Ws_Conn/,
                  filter: /ngs-edge/,
                  filterArgIndex: 0,
                  func: (args) => { wsBinaryCounter = 0; return "STREAM_ON_CONNECT" }
              },
              {
                  callType: /^NETWORK_Ws_Open/,
                  filter: /ngs-edge/,
                  filterArgIndex: 0,
                  func: (args) => "STREAM_ON_CONNECTED"
              },
              {
                  callType: /^NETWORK_Ws_Msg/,
                  filter: /"eventType":"onServerInfo"/,
                  filterArgIndex: 1,
                  func: (args) => "STREAM_ON_SERVER_INFO"
              },
              {
                  callType: /^NETWORK_Ws_Msg/,
                  filter: /"onStreamStatus":/,
                  filterArgIndex: 1,
                  func: (args) => { return "STREAM_ON_STREAM_STATUS " + (args[1].includes('isColdStarted":true') ? "☁" : ( args[1].includes('isColdStarted":false') ? "🔥" : "?")) }
              },
              {
                  callType: /^NETWORK_Ws_Msg/,
                  filter: /"eventType":"onStreamInfo"/,
                  filterArgIndex: 1,
                  func: (args) => "STREAM_ON_STREAM_INFO"
              },
              {
                  callType: /^NETWORK_Ws_Msg/,
                  filter: /ngs-edge/,
                  filterArgIndex: 0,
                  func: (args) => args[2] && ++wsBinaryCounter == 2 ? "STREAM_RECEIVE_KEYFRAME" : null
              },
              {
                  callType: /^NETWORK_Video_LoadedData/,
                  filter: /.{2,}/,
                  filterArgIndex: 0,
                  func: (args) => { return args[0].length > 0 ? "STREAM_MEDIA_LOADED_DATA" : null } // empty video id filtered out because it's some preroll stuff
              },
              {
                  callType: /^NETWORK_Video_Playing/,
                  filter: /.{2,}/,
                  filterArgIndex: 0,
                  func: (args) => { return args[0].length > 0 ? "STREAM_MEDIA_PLAYING" : null } // empty video id filtered out because it's some preroll stuff
              }
          ]; // these steps will be measured and shown when reaches the last step

    // SCRIPT

    console.log("MEASUREMENT");

    // UI


    const createFloating = ( id, style, addClose ) =>
    {
        const div = document.createElement('div');
        div.id = id;
        Object.assign(div.style, {width: '210px', height: '100px', color: "white", background: '#700000', position: 'absolute', top: '50px', left: '50px', cursor: 'move', zIndex: 100000 });
        Object.assign(div.style, style);

        const content = document.createElement("div");
        div.appendChild( content );

        if (addClose)
        {
            const close = document.createElement("div");
            close.textContent = "x";
            close.className = "measurement-close";
            Object.assign(close.style, { position: "absolute", padding: "5px", textAlign: "center", lineHeight: "12px", top: "0px", right: "5px", color: "#999999", cursor: "pointer" } );
            close.onclick = () => { div.style.display = "none" }
            div.appendChild(close);
        }

        let isDown = false, offsetX, offsetY;
        div.addEventListener('mousedown', e => { isDown = true; offsetX = e.offsetX; offsetY = e.offsetY; });
        document.addEventListener('mousemove', e => { if (isDown) Object.assign(div.style, {left: e.pageX - offsetX + 'px', top: e.pageY - offsetY + 'px'}); });
        document.addEventListener('mouseup', () => { isDown = false });
        return div;
    }

    const measurementIcon = createFloating( "measurementFloatingIcon", {left: 0, top: 0, cursor: "poiner", borderRadius: "50%", width: "35px", height: "35px", border: "1px solid #500000", background: "#700000", color: "white", textAlign: "center", lineHeight: "35px", userSelect: "none" } );
    measurementIcon.firstElementChild.textContent = "M";
    const display = localStorage.getItem("networkMeasuermentDisplay") == "true";
    const floatingBox = createFloating( "measurementFloatingBox", {display: display ? "box" : "none", whiteSpace: "pre", fontFamily: "monospace", padding: "5px", fontSize: "11px", height: "auto", minHeight: "100px", borderRadius: "2px"}, true );
    measurementIcon.addEventListener("click", () => { floatingBox.style.display == "none" ? floatingBox.style.display = "block" : floatingBox.style.display = "none"; localStorage.setItem("networkMeasuermentDisplay", floatingBox.style.display != "none"); });

    floatingBox.querySelector(".measurement-close").addEventListener("click", () => { localStorage.setItem("networkMeasuermentDisplay", floatingBox.style.display != "none"); } );

    const enableLogsCheckbox = document.createElement('input');
    enableLogsCheckbox.type = "checkbox";
    enableLogsCheckbox.title = "Enable console logs";
    enableLogsCheckbox.checked = networkLogsEnabled;
    enableLogsCheckbox.onchange = () => { networkLogsEnabled = !networkLogsEnabled; localStorage.setItem("networkMeasuermentConsoleLogs", networkLogsEnabled); };
    Object.assign(enableLogsCheckbox.style, { position: "absolute", top: "5px", right: "20px", width: "12px", height: "12px" });
    floatingBox.appendChild(enableLogsCheckbox);

    // Measurements
    const printMeasurements = () =>
    {
        if (measurements.length < 3 )
        {
            return;
        }

        const pad = 27;
        let result = "";
        let sumTime = 0;
        for ( let i = 1; i < measurements.length; i++ )
        {
            let diffTime = measurements[i].time - measurements[i-1].time;
            sumTime += diffTime;
            result += measurements[i].msg.padEnd(pad) + " " + (diffTime).toString().padStart(4) + "\n";
        }

        const measurementText = 'MEASUREMENTS'.padEnd(pad) + '\n' + '____________'.padEnd(pad) + '\n' + result + '\n' + 'SUM'.padEnd(pad) + ' ' + sumTime.toString().padStart(4);

        console.log('%c' + measurementText, 'background: red; color: #bada55');
        floatingBox.firstElementChild.textContent = measurementText;
    }

    const addMeasurement = (callType, ...args) =>
    {
        let highlighted = false;

        for ( let i = 0; i < measurementConfig.length; i++ )
        {

            if (measurementConfig[i].callType.test(callType) && (!measurementConfig[i].filter || !measurementConfig[i].filterArgIndex || measurementConfig[i].filter.test(args[measurementConfig[i].filterArgIndex])))
            {

                let pushed = false;
                if ( (pushed = pushMeasurement( measurementConfig[i].func(args) )) && i == 0 )
                {
                    measurements = measurements.slice(-1); // reset if it was the first real action
                }

                if (pushed)
                {
                    highlighted = true;
                    networkLogsEnabled && console.log('%c' + callType + " " + JSON.stringify(args), 'background: darkorange; color: white');
                }

                if ( i == measurementConfig.length - 1 && pushed )
                {
                    printMeasurements(); // print if it was the last
                    measurements = [];
                }

                break;
            }
        }

        if ( networkLogsEnabled && !highlighted )
        {
            console.log( callType, ...args );
        }
    }



    // Image listeners
    const OrigImage = window.Image;
    window.Image = function(...args) { const img = new OrigImage(...args); img.addEventListener('load', function() {addMeasurement('NETWORK_Img2', img.src);}); return img; };

    function handleNewImages(images) {
        images.forEach(img => {
            addMeasurement('NETWORK_Img', img.src);
        });
    }

    const loadedListener = (e) => { addMeasurement('NETWORK_Video_LoadedData', e.target.id); }
    const playingListener = (e) => { addMeasurement('NETWORK_Video_Playing', e.target.id); }

    function handleNewVideos(videos) {
        videos.forEach(video => {
            //video.style.opacity = 0.1;
            video.addEventListener("loadeddata", loadedListener);
            video.addEventListener("playing", playingListener );
        });
    }

    const observer = new MutationObserver(mutations => { // Create a MutationObserver to watch for added nodes
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(node => {
                    if (node.tagName === 'IMG') {
                        handleNewImages([node]);
                    } else if (node.querySelectorAll) {
                        // Check for images within added containers
                        const imgs = node.querySelectorAll('img');
                        if (imgs.length > 0) {
                            handleNewImages(imgs);
                        }
                    }
                    if (node.tagName === 'VIDEO') {
                        handleNewVideos([node]);
                    } else if (node.querySelectorAll) {
                        // Check for images within added containers
                        const videos = node.querySelectorAll('video');
                        if (videos.length > 0) {
                            handleNewVideos(videos);
                        }
                    }
                });
            }
        });
    });

    window.addEventListener("load", () => {

        setTimeout( () => {
            document.body.appendChild(floatingBox);
            document.body.appendChild(measurementIcon);
        }, 1000);

        observer.observe(document.body, { // Start observing the document body for changes
            childList: true,
            subtree: true
        });

    });


    // User listeners
    document.addEventListener("click", (event) => { addMeasurement("NETWORK_User_Click", event.target); });
    document.addEventListener("mousedown", (event) => { addMeasurement("NETWORK_User_MouseDown", event.target); });
    document.addEventListener("mouseup", (event) => { addMeasurement("NETWORK_User_MouseUp", event.target); });
    document.addEventListener("touchstart", (event) => { addMeasurement("NETWORK_User_TouchStart", event.target); });
    document.addEventListener("touchend", (event) => { addMeasurement("NETWORK_User_TouchEnd", event.target); });

    // Request listeners
    const origFetch = fetch;
    window.fetch = (...args) => { addMeasurement("NETWORK_Fetch", ...args); return origFetch(...args); }

    const origOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (...args) { addMeasurement("NETWORK_Xhr", ...args); return origOpen.apply(this, args); }

    // Websocket listeners
    const origWsSend = WebSocket.prototype.send;
    WebSocket.prototype.send = function(data)
    {
        if (data != 3 && !filteredWs.test(this.url))
        {
            addMeasurement("NETWORK_Ws_Send", this.url, data);
        }

        if ( !(data instanceof ArrayBuffer) && injectWsSend.pattern.test(data))
        {
            data = data.replace(injectWsSend.pattern, injectWsSend.replacement);
        }

        return origWsSend.call(this, data);
    }

    const OrigWs = window.WebSocket;
    window.WebSocket = function (url, protocols)
    {
        let ws = new OrigWs(url, protocols);
        if (!filteredWs.test(url)) {
            let binaryCount = 0;
            let filteredMsgCount = 0;
            addMeasurement("NETWORK_Ws_Conn", url, protocols);
            ws.addEventListener("open", (event) => { addMeasurement("NETWORK_Ws_Open", url); });
            ws.addEventListener("close", (event) => { addMeasurement("NETWORK_Ws_Close", url); });

            ws.addEventListener("message", (event) =>
                {
                    const isBinary = event.data instanceof ArrayBuffer;
                    const eventFilterTest = wsEventFilter.r.test(event.data);
                    if (event.data != 2 && ((isBinary && binaryCount++ < binaryWsPackageCountLimit) || (!isBinary && (!eventFilterTest || filteredMsgCount++ < wsEventFilter.limit))))
                    {
                        addMeasurement("NETWORK_Ws_Msg", url, event.data, isBinary);
                    }
                }
            );
        }
        return ws;
    }
})();