micmro / performance-bookmarklet

Performance-Bookmarklet helps to analyze the current page through the Resource Timing API, Navigation Timing API and User-Timing - requests by type, domain, load times, marks and more. Sort of a light live WebPageTest.
MIT License
1.14k stars 87 forks source link

"sandbox" the performance bookmarklet #47

Open MorrisJohns opened 5 years ago

MorrisJohns commented 5 years ago

I have modified the performance bookmarklet so that it runs within an iframe with it's own security sandbox (so that the code is only loaded as needed, and so I don't need to audit your code much!). The performance data is passed from the "host" document to the "guest" iframe using sendMessage.

I have attached the code here to give an idea of the changes, although it would need a few hours work for anyone to use.

The function to show the performance metrics (e.g. connected to a button), run from the "host" document is:

function showTiming() {
    function objectify(object) {
        return JSON.parse(JSON.stringify(object));
    }
    var performance = window.performance;
    if (!performance || !performance.getEntriesByType) return;
    var perf = {
        resources: objectify(performance.getEntriesByType('resource')),
        marks: objectify(performance.getEntriesByType('mark')),
        measures: objectify(performance.getEntriesByType('measure')),
        timing: objectify(performance.timing),
        navigation: objectify(performance.navigation)
    };

    var html = '<script src="https://some.domain.here/performance-bookmarklet.js"></script>';
    var secondBody = document.createElement('body');
    secondBody.innerHTML = '<iframe class=nobox frameborder=0 src="about:blank"></iframe>';
    var iframe = secondBody.firstChild;
    if ('srcdoc' in iframe) {
        iframe.setAttribute('sandbox', 'allow-scripts');
        iframe.setAttribute('referrerpolicy', 'no-referrer');
        iframe.srcdoc = html;
    } else {    // srcdoc not IE but will be introduced in Edge 18
        setTimeout(function() { // IE needs iframe to be in document so use postpone
            var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
            iframeDoc.open();
            iframeDoc.write(html);
            iframeDoc.close();
        }, 0);
    }
    iframe.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;z-index:10000;';
    iframe.onload = function() {
        iframe.contentWindow.postMessage(perf, '*');
        iframe.onload = null;
    };
    var closeButton = document.createElement('button');
    closeButton.textContent = 'close';
    closeButton.style.cssText = 'position:fixed;top:20px;left:20px;z-index:10001;background-color:#BBF;';
    closeButton.onclick = function() {
        document.body.style.overflow = '';
        document.documentElement.removeChild(secondBody);
    }
    secondBody.appendChild(closeButton);
    document.body.style.overflow = 'hidden';
    document.documentElement.appendChild(secondBody);   // looks odd, but html spec does allow multiple body elements
}

I edited the compiled performance-bookmarklet.js (Yes this is a very dirty technique. Done because I didn't want to edit source code as then I would need to run your build process which would have taken me more time and I needed the code to work with our custom build process).

We put the following code around the performance-bookmarklet.js code (dirty: monkey patches window.performance to read the local data)

(function () {
    function go(perf) {
        window.performance = {
            timing: perf.timing,
            navigation: perf.navigation,
            getEntriesByType: function(type){
                if (type == "resource") {
                    return perf.resources;
                } else if (type == 'mark') {
                    return perf.marks;
                } else if (type == "measure") {
                    return perf.measures;
                }
            }
        };
        execBookmarklet(withinIframe);
        window.performance = null;
    }

    if (window.doubleLoad) {
        return;
    }
    window.doubleLoad = 1;

    var withinIframe = window.top !== window.self;
    if (withinIframe) {
        window.onmessage = function(event) {
            var perf = event.data;
            if (perf.resources) {
                go(perf);
            }
        };
    } else {    // Called by 'Timing Graph Debug'
        var objectify = function(object) {
            return JSON.parse(JSON.stringify(object));
        }
        go({
            resources: objectify(window.performance.getEntriesByType("resource")),  // JSON.parse(JSON.stringify(window.performance.getEntriesByType("resource")))
            marks: objectify(window.performance.getEntriesByType("mark")),
            measures: objectify(window.performance.getEntriesByType("measure")),
            timing: objectify(window.performance.timing),
            navigation: objectify(window.performance.navigation)
        });
    }

    // put copyright into a string stored on an object so that the copyright isn't removed during compression
    execBookmarklet.copyright = 'Copyright 2014 Michael Mrowetz. MIT License: https://github.com/micmro/performance-bookmarklet/blob/d3e037620270419521eab20bf479c51c8ea72d96/LICENSE';

function execBookmarklet(withinIframe) {
// INSERT BOOKMARKLET CODE HERE
}
})();

The BOOKMARKLET CODE itself also has a few modifications.

Don't read localStorage (causes exception):

if (!withinIframe)
    var persistanceEnabled = !!JSON.parse(localStorage.getItem(storageKey));

Change the IFrameHolder code:

iFrameHolder.getOutputIFrame = function () {
    return outputIFrame;
};

module.exports = iFrameHolder;

if (withinIframe) {
    module.exports = {
        setup: function (onIFrameReady) {
            function addComponent(domEl) {
                document.body.appendChild(domEl);
            }
            var styleTag = dom.newTag("style", {
                type: "text/css",
                text: style
            });
            document.head.appendChild(styleTag);
            var outputHolder = dom.newTag("div", { id: "perfbook-holder" });
            var outputContent = dom.newTag("div", { id: "perfbook-content" });
            outputHolder.appendChild(outputContent);
            document.body.appendChild(outputHolder);
            onIFrameReady(addComponent);
        },
        getOutputIFrame: function(){
            return document;
        }
    };
}

Add connection details (very important for HTTP2 connection versus HTTP1.1 connection):

    var connection = navigator.connection;
    if (connection) {
        createAppendixDefValue(appendix, dom.newTag("abbr", { title: "Connection", text: "navigator.connection.*" }), "");
        for (var key in connection) {
            var value = connection[key];
            if (value && typeof value != "function") {
                createAppendixDefValue(appendix, dom.newTextNode("\xa0\xa0" + key + ":"), value);
            }
        }
    }

    tilesHolder.appendChild(appendix);
    return tilesHolder;

Dont check for about: protocol:

if (withinIframe) {
    if (!data.isValid()) {
        return;
    }
} else {
    if (location.protocol === "about:" || !data.isValid()) {
        return;
    }
}

Commented out dead code:

        // window.outputContent;

The final change was to remove the negative right margins (replaced -18px and -72px with 0 in the css style).

I have written this issue so that if anyone else wants to use your code others can branch and build on these snippets. Hopefully it is useful to someone so maybe just leave the issue open?!

I really really appreciate your code - it still works beautifully (2019).

MorrisJohns commented 5 years ago

Uploaded edited performance-bookmarklet.js.txt with the changes made. Compare changes against the old version of performanceBookmarklet.js it was based on: https://github.com/micmro/performance-bookmarklet/blob/36cc6ee1afcfaf1a1a85933affe317e2afb18738/dist/performanceBookmarklet.js

micmro commented 5 years ago

Hi @MorrisJohns thanks a lot for your work. I will give it a proper review and update the code accordingly, but because I am moving countries in the moment I will unfortunately need another few weeks before I will get the time for this.