Bubble Logger

log uncaught window (XHR.send, XHR.onerror) exceptions and write them in the document as bootstrap alert html elements

// ==UserScript==
// @name         Bubble Logger
// @require      https://code.jquery.com/jquery-3.4.1.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/renderjson.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/rxjs/8.0.0-alpha.2/rxjs.umd.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/d3.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js
// @run-at       document-end
// @namespace    http://tampermonkey.net/
// @version      1.1
// @license      MIT
// @description  log uncaught window (XHR.send, XHR.onerror) exceptions and write them in the document as bootstrap alert html elements
// @author       Sloppy Lo
// @match        http*://*/*
// @icon         https://store-images.s-microsoft.com/image/apps.32031.13510798887630003.b4c5c861-c9de-4301-99ce-5af68bf21fd1.ba559483-bc2c-4eb9-a17e-c302009b2690?w=180&h=180&q=60
// @resource     REMOTE_BOOTSTRAP_CSS https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css
// @resource     ANIMATE_CSS_MIN https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css
// @grant        GM_xmlhttpRequest
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @grant        unsafeWindow
// ==/UserScript==
// Info: https://sourceforge.net/p/greasemonkey/wiki/unsafeWindow/
//       https://benjamine.github.io/jsondiffpatch/demo/index.html
//       https://abodelot.github.io/jquery.json-viewer/
//       https://programming.mediatagtw.com/article/tampermonkey+unsafewindow
//       https://stackoverflow.com/questions/2631820/how-do-i-ensure-saved-click-coordinates-can-be-reload-to-the-same-place-even-if/2631931#2631931
//       http://jsfiddle.net/luisperezphd/L8pXL/
//       /nl/wlan-access-points/mikrotik/omnitik-5-poe-ac-rbomnitikpg-5hacd-art-rbomnitikpg-5hacd-num-6166159/
//       https://theonlytutorials.com/how-to-make-a-div-movable-draggable/ Missing feature: DRAGGABLE
//       https://wiki.greasespot.net/Content_Script_Injection
// IIFE
(function() {
    //Init
    "use strict";
    let id = 0;
    let masterId = 0;
    let scrolling = false;
    let stats = [];
    let timeoutId = null;
    let canonicalLinkURI, windowURI;
    let firstRun = true;
    const constants = {
      GTM: "GTM",
      ENV: "ENV"
    };
    const { GTM, ENV } = constants;
    const cache = [];
    const isIframe = (window.self === window.top);
    const $ = window.jQuery;
    const rxjs = window.rxjs;
    const axios = window.axios;
    window.dataLayer = window.dataLayer || [];
    const bubbleStates = ["primary", "danger", "warning"];
    let open = XMLHttpRequest.prototype.open;
    let send = XMLHttpRequest.prototype.send;
    console.defaultError = console.error.bind(console);
    console.errors = [];
    console.defaultWarn = console.warn.bind(console);
    console.warns = [];
    console.defaultInfo = console.info.bind(console);
    console.infos = [];
    //IIFE: Add non-rewritable styles to avoid getting affected by target website CSS's
    (function() {
      // Load remote CSS
      // @see https://github.com/Tampermonkey/tampermonkey/issues/835
      const overwriteBootrapDismissableButtonCSS = `.alert-dismissible .close {
                                                      position: absolute !important;
                                                      top: 0 !important;
                                                      right: 0 !important;
                                                      padding: 0.75rem 1.25rem !important;
                                                      color: inherit !important;
                                                      width: 40px !important;
                                                      box-shadow: none !important;
                                                      font-size: 1.5rem !important;
                                                    }`;
      GM_addStyle(overwriteBootrapDismissableButtonCSS);
      const bootstrapCss = GM_getResourceText("REMOTE_BOOTSTRAP_CSS");
      GM_addStyle(bootstrapCss);
      const animateCssMin = GM_getResourceText("ANIMATE_CSS_MIN");
      GM_addStyle(animateCssMin);
      const jsonViewerCss = `.renderjson a { text-decoration: none; }
                            .renderjson .disclosure { color: green;
                             font-size: 150%; }
                            .renderjson .syntax { color: grey; }
                            .renderjson .string { color: darkred; }
                            .renderjson .number { color: darkcyan; }
                            .renderjson .boolean { color: blueviolet; }
                            .renderjson .key    { color: darkblue; }
                            .renderjson .keyword { color: blue; }
                            .renderjson .object.syntax { color: lightseagreen; }
                            .renderjson .array.syntax  { color: orange; }
                            .containerErrors pre  { padding: 0 !important; background: no-repeat !important}`;
      GM_addStyle(jsonViewerCss);
      const customCollapse = ".customCollapse {float: right !important}";
      GM_addStyle(customCollapse);
      const spinnerCss = ".loadingio-spinner-bean-eater-m1d52hd0p4d {top:50% !important; left:50% !important} a {display: inline !important}";
      GM_addStyle(spinnerCss);
      const errorMessageCss = ".alert {word-break: break-word !important; opacity: 0.95 !important; margin: 0px !important; font-size:13px !important}";
      GM_addStyle(errorMessageCss);
      const requestsBoxCssWidth = (isIframe) ? "388px" : "351px";
      const requestsBoxCss = `.requestsBox {
                                overflow-y: scroll !important;
                                max-height: 88% !important;
                                width: ${requestsBoxCssWidth} !important;
                                position: fixed !important;
                                top: 0% !important;
                                z-index: 99999999
                                !important;
                                left: 0;
                              } .string {word-wrap: break-word !important;}`;
      GM_addStyle(requestsBoxCss);
      const spinnerDivCss = `.spinnerDiv {
                                      width: 10% !important;
                                      float: left;
                                      background-color: #1f1f22 !important
                              }`;
      GM_addStyle(spinnerDivCss);
      const buttonsDivCss = ".buttonsDiv {overflow-y: scroll !important;display: none; width: 100% !important; position: relative !important; float: left !important;} .buttonsDiv a {width: 33.3% !important; height: 31px !important; float: left !important; text-align: center !important; font-size: 12px !important; border-color: black !important; padding-top: 5px !important}";
      GM_addStyle(buttonsDivCss);
      // const responsesBoxCss = ".responsesBox {position: fixed !important; right: 0% !important; top: 70% !important; width: 50% !important; z-index: 99999999 !important; overflow-y: scroll !important;}";
      // GM_addStyle(responsesBoxCss);
      const top = (isIframe) ? "7.5%" : "26%";
      const containerErrorsCSSWidth = (isIframe) ? "388px" : "351px";
      const containerErrorsCss = `.containerErrors {
                                      display: none;
                                      max-height: 752px !important;
                                      width: ${containerErrorsCSSWidth} !important;
                                      top: ${top} !important;
                                      position: fixed !important;
                                      overflow-y: scroll !important;
                                      z-index: 99999999 !important;
                                   }
                                   .containerErrors div {
                                     font-style: normal !important;
                                   }`;
      GM_addStyle(containerErrorsCss);
      const containerSvgCss = `.svgContainer {
                                    cursor: pointer !important;
                                    left: 1%;
                                    z-index: 99999999 !important;
                               }
                               .svgContainer svg{
                                    float: left
                               }`;
      GM_addStyle(containerSvgCss);
      const objectMessagePCss = ".objectMessageP {word-break: break-word;font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji !important; font-size: 13px !important}";
      GM_addStyle(objectMessagePCss);
      const width = (isIframe) ? "90%" : "90%";
      const replDivCss = `.replDiv {
                                  display: none;
                                  height: 50px !important;
                                  width: ${width} !important;
                                  float: left !important
                          }
                          .replDiv input {
                                  background-color: #1f1f22 !important;
                                  height: 100% !important;
                                  width: 100% !important;
                                  font-size: 18px !important;
                                  border: 0 !important;
                          }`;
      GM_addStyle(replDivCss);
    })();

    //Create reference to elements
    const spinner = $("<div class=\"spinnerDiv\"><svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" style=\"margin: auto; background: none; display: block; shape-rendering: crispedges; animation-play-state: running; animation-delay: 0s;\" width=\"50px\" height=\"50px\" viewBox=\"0 0 100 100\" preserveAspectRatio=\"xMidYMid\">  <g style=\"animation-play-state: running; animation-delay: 0s;\">    <circle cx=\"60\" cy=\"50\" r=\"4\" fill=\"#ffffff\" style=\"animation-play-state: running; animation-delay: 0s;\">      <animate attributeName=\"cx\" repeatCount=\"indefinite\" dur=\"1s\" values=\"95;35\" keyTimes=\"0;1\" begin=\"-0.67s\" style=\"animation-play-state: running; animation-delay: 0s;\"></animate>      <animate attributeName=\"fill-opacity\" repeatCount=\"indefinite\" dur=\"1s\" values=\"0;1;1\" keyTimes=\"0;0.2;1\" begin=\"-0.67s\" style=\"animation-play-state: running; animation-delay: 0s;\"></animate>    </circle>    <circle cx=\"60\" cy=\"50\" r=\"4\" fill=\"#ffffff\" style=\"animation-play-state: running; animation-delay: 0s;\">      <animate attributeName=\"cx\" repeatCount=\"indefinite\" dur=\"1s\" values=\"95;35\" keyTimes=\"0;1\" begin=\"-0.33s\" style=\"animation-play-state: running; animation-delay: 0s;\"></animate>      <animate attributeName=\"fill-opacity\" repeatCount=\"indefinite\" dur=\"1s\" values=\"0;1;1\" keyTimes=\"0;0.2;1\" begin=\"-0.33s\" style=\"animation-play-state: running; animation-delay: 0s;\"></animate>    </circle>    <circle cx=\"60\" cy=\"50\" r=\"4\" fill=\"#ffffff\" style=\"animation-play-state: running; animation-delay: 0s;\">      <animate attributeName=\"cx\" repeatCount=\"indefinite\" dur=\"1s\" values=\"95;35\" keyTimes=\"0;1\" begin=\"0s\" style=\"animation-play-state: running; animation-delay: 0s;\"></animate>      <animate attributeName=\"fill-opacity\" repeatCount=\"indefinite\" dur=\"1s\" values=\"0;1;1\" keyTimes=\"0;0.2;1\" begin=\"0s\" style=\"animation-play-state: running; animation-delay: 0s;\"></animate>    </circle>  </g><g transform=\"translate(-15 0)\" style=\"animation-play-state: running; animation-delay: 0s;\">  <path d=\"M50 50L20 50A30 30 0 0 0 80 50Z\" fill=\"#005bbf\" transform=\"rotate(90 50 50)\" style=\"animation-play-state: running; animation-delay: 0s;\"></path>  <path d=\"M50 50L20 50A30 30 0 0 0 80 50Z\" fill=\"#005bbf\" style=\"animation-play-state: running; animation-delay: 0s;\">    <animateTransform attributeName=\"transform\" type=\"rotate\" repeatCount=\"indefinite\" dur=\"1s\" values=\"0 50 50;45 50 50;0 50 50\" keyTimes=\"0;0.5;1\" style=\"animation-play-state: running; animation-delay: 0s;\"></animateTransform>  </path>  <path d=\"M50 50L20 50A30 30 0 0 1 80 50Z\" fill=\"#005bbf\" style=\"animation-play-state: running; animation-delay: 0s;\">    <animateTransform attributeName=\"transform\" type=\"rotate\" repeatCount=\"indefinite\" dur=\"1s\" values=\"0 50 50;-45 50 50;0 50 50\" keyTimes=\"0;0.5;1\" style=\"animation-play-state: running; animation-delay: 0s;\"></animateTransform>  </path></g>  <!-- [ldio] generated by https://loading.io/ --></svg></div>");
    const replDiv = $("<div id=\"replDiv\" class=\"replDiv\"><input type=\"text\" placeholder=\"REPL: alert('Happy Hacking!')\" ></input></div>");
    const requestsBox = $("<div id=\"requestsBox\" draggable = \"true\" class=\"requestsBox animate__animated animate__rollIn customCollapse\">");
    // const responsesBox = $("<div class=\"responsesBox\">");
    const containerSvg = $("<div id=\"svgContainer\" class=\"svgContainer\">");
    const containerErrors = $("<div id=\"containerErrors\" class=\"containerErrors animate__animated\">");
    const hrefButton = $("<a class=\"btn btn-primary\" style=\"background-image: none !important;\" role=\"button\">href</a>");
    const refreshButton = $("<a class=\"btn btn-primary\" style=\"background-image: none !important;\" onclick=\"window.location.reload(true)\" role=\"button\">refresh</a>");
    const closeButton = $("<a class=\"btn btn-danger\" style=\"background-image: none !important;\" role=\"button\">X</a>");
    const buttonsDiv = $("<div class=\"buttonsDiv\"></div>");
    refreshButton.prependTo(buttonsDiv);
    hrefButton.prependTo(buttonsDiv);
    closeButton.prependTo(buttonsDiv);

    spinner.prependTo(containerSvg);
    replDiv.on("click", repl);
    replDiv.appendTo(containerSvg);
    buttonsDiv.appendTo(containerSvg);
    containerSvg.prependTo(requestsBox);
    requestsBox.appendTo($("body"));
    containerErrors.appendTo($("body"));

    //Make element Draggable W.I.P
    requestsBox[0].ondragstart = (event) => {
      event.dataTransfer.setData("text", event.target.id);
    };
    const body = $("body");
    if (body) {
      body.ondrop = (event) => {
        event.preventDefault();
        let data = event.dataTransfer.getData("text");
        event.target.appendChild(document.getElementById(data));
      };
      body.ondragover = (event) => {
        event.preventDefault();
      };
    }

    //Handcraft 'out' animation for requestsBox(This is not easy to modify)
    function closeContainerErrorsWithAnimations(closeButtons = false, collapseSpinner = false) {
      $(containerErrors).addClass("animate__hinge");
      if (closeButtons) {
        $(buttonsDiv).css("height", "500px");
        $(buttonsDiv).addClass("animate__hinge");
      }
      setTimeout(() => {
        $(containerErrors).removeClass("animate__hinge");
        $(buttonsDiv).removeClass("animate__hinge");
        $(buttonsDiv).css("height", "");
      }, 2000);
      setTimeout(() => {
        if (closeButtons) {
          $(requestsBox).addClass("customCollapse");
          $(".replDiv").css("display", "none");
        }
        $(".containerErrors div").fadeOut("2000");
        if (collapseSpinner) {
          $(buttonsDiv).css("display", "none");
          $(containerErrors).css("display", "none");
        }
      }, 1000);
    }

    //Spinner Click handler
    spinner.on("click", function() {
      //Handcraft 'in' animation for requestsBox
      if ($(requestsBox).hasClass("customCollapse")) {
        $(containerErrors).css("display", "block");
        $(requestsBox).removeClass("customCollapse");
        $(".containerErrors div").fadeIn("2000");
        $(".replDiv").css("display", "block");
        $(requestsBox).css("left", "0%");
        $(buttonsDiv).css("display", "block");
        $(containerErrors).css("display", "block");
      } else {
        closeContainerErrorsWithAnimations(true, true);
      }
    });
    // Add close button animation
    closeButton.on("click", () => {
      closeContainerErrorsWithAnimations();
    });
    // Add HREF button functionality
    hrefButton.on("click", function() {

      // const buttons = $("button");
      // buttons.map((index, button) => {
      // if($(button).data("events").click){
      //   $(button).css("background", "red");
      //   $(button).css("border-color", "yellow");
      //   $(button).css("border", "2px");
      // }
      // });
      try {
        const anchors = $("a[href^=\"/\"], a[href*=\"" + window.location.host + "\"]")
          .not(".svgContainer a")
          .not(".containerErrors a")
          .not("a[href=\"#\"]");
        const corsAnchors = $("a[href^=\"http\"]")
          .not("a[href*=\"" + window.location.host + "\"]")
          .not("a[href^=\"/\"]")
          .not(".svgContainer a")
          .not(".containerErrors a")
          .not("a[href=\"#\"]");
        // d3.selectAll(anchors).style("background-color", function() {
        //   return "hsl(" + Math.random() * 360 + ",100%,50%)";
        // });
        if (corsAnchors) {
          corsAnchors.map((index, corsAnchor) => {
            d3.select(corsAnchor).transition().duration(750)
              .style("background-color", "#ffc107")
              .style("border", "5px")
              .style("border-color", "#ffc107");
          });
        } else {
          console.warn("No CORS anchors found");
        }
        if (anchors) {
          anchors.map(async (index, anchor) => {
            try {
              await axios.get(anchor.href,{
                maxRedirects: 0,
                validateStatus: null})
              d3.select(anchor).transition().duration(500)
                .style("background-color", "green")
                .style("border", "5px")
                .style("border-color", "green");
            } catch (error) {
              d3.select(anchor).transition()
                .style("background-color", "red")
                .style("border", "5px")
                .style("border-color", "red");
            }
          });
        } else {
          console.warn("No anchors found");
        }
        // d3.select("body").transition()
        //   .style("background-color", "black");
        // d3.selectAll(anchors).transition()
        //   .duration(750)
        //   .delay(function(d, i) {
        //     return i * 10;
        //   })
        //   .attr("r", function(d) {
        //     return Math.sqrt(d * 1);
        //   });
      } catch (e) {
        console.error(e);
      }
      // $(anchors).css("background", "red").css("border-color", "yellow").css("border", "2px");
    });

    //Hijack error
    console.error = function() {
      renderEventInHTML(arguments[0], "danger");
      // default &  console.error()
      console.defaultError.apply(console, arguments);
      // new & array data
      console.errors.push(Array.from(arguments));
    };
    //Hijack warn
    console.warn = function() {
      renderEventInHTML(arguments[0], "warning");
      // default &  console.error()
      console.defaultWarn.apply(console, arguments);
      // new & array data
      console.warns.push(Array.from(arguments));
    };
    //Hijack info
    console.info = function() {
      renderEventInHTML(arguments[0], "primary");
      // default &  console.error()
      console.defaultInfo.apply(console, arguments);
      // new & array data
      console.infos.push(Array.from(arguments));
    };

    //Handler for window unhandledrejection, rejectionhandled, error
    function logEvent(errorEvent, type) {
      let error = undefined;
      try {
        if (errorEvent.reason) {
          error = errorEvent.reason.stack || errorEvent.reason.message;
        } else {
          error = errorEvent.error || errorEvent.message || JSON.stringify(errorEvent);
        }
      } catch (e) {
        console.log(e);
      }
      console.error(`window.${type}: ${error}`);
    }

    // this is only to distinguish http errors by status, cannot be used to distinguish every single event we might get
    function sendToConsole(stats) {
      stats.forEach((stat) => {
        if (stat.statusCode >= 200 && stat.statusCode < 400) {
          console.info(stat);
        } else if (stat.statusCode === 404) {
          console.warn(stat);
        } else {
          console.error(stat);
        }
      });
    }

    //TODO, We need to return promises here to be able to syncronize the event ids
    //Add custom Event listener to window, XMLHttpRequest
    function addCustomEventListeners() {
      let timer = null;
      if (window) {
        let _onerror = function(event, url, lineNo, columnNo, error) {
          let message = [];
          let eventMessage = event.message ? event.message.toLowerCase() : event.error ? event.error : null;
          if (!eventMessage && event.path[0].tagName === "IMG") {//This is hacky, change me
            eventMessage = event.type + " at <a href=\"#\" style=\"word-break: break-word;\">" + new Option(event.path[0].outerHTML).innerHTML + "<a\>";
          }
          if (!eventMessage) {
            eventMessage = event.target.id || event.target.src;
          }
          let substring = "script error";
          if (eventMessage.indexOf(substring) > -1) {
            alert("Script Error: See Browser Console for Detail");
          } else {
            message = [
              masterId,
              " Message: " + eventMessage,
              "URL: " + url,
              "Line: " + lineNo,
              "Column: " + columnNo,
              "Error object: " + JSON.stringify(error)
            ].join(" - ");
            console.error(message);
          }
          return false;
        };
        window.onerror = function() {
          let args = Array.prototype.slice.call(arguments);
          if (_onerror) {
            return _onerror.apply(window, args);
          }
          return false;
        };
        $.error = function(message) {
          console.error("jQuery: " + message);
        };
        window.onscroll = function(event) {
          event.preventDefault();
          event.stopPropagation();
          if (!scrolling) {
            scrolling = true;
            masterId++;
          }
          if (timer !== null) {
            clearTimeout(timer);
          }
          timer = setTimeout(function() {
            scrolling = false;
          }, 1000);

          // if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
          //   // you're at the bottom of the page
          // }
        };
        window.addEventListener("unhandledrejection", function(errorEvent) {
          logEvent(errorEvent, "unhandledrejection");
        });
        window.addEventListener("rejectionhandled", function(errorEvent) {
          logEvent(errorEvent, "rejectionhandled");
        });
        window.addEventListener("error", function(errorEvent) {
          logEvent(errorEvent, "error");
        });
        XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
          this._url = url;
          this._method = method;
          open.call(this, method, url, async, user, pass);
        };
        XMLHttpRequest.prototype.send = function(data) {
          let self = this;
          let start;
          let oldOnReadyStateChange;
          let url = this._url;
          let method = this._method;

          function onReadyStateChange(event) {
            //Info: Log all you need from event
            if (self.readyState === 4 /* complete */) {
              let time = new Date() - start;
              stats.push({
                url: url,
                id: masterId,
                duration: time + "ms",
                statusCode: ["undefined", "null", "", null, undefined].includes(event.currentTarget.status) ? "unknown" : event.currentTarget.status,
                response: ["undefined", "null", "", null, undefined].includes(event.currentTarget.response) ? "unknown" : event.currentTarget.response,
                method: event.currentTarget["nr@context"] && event.currentTarget["nr@context"].params ? event.currentTarget["nr@context"].params.method : method
              });
              sendToConsole(stats);
              timeoutId = null;
              stats = [];
            }
            if (oldOnReadyStateChange) {
              oldOnReadyStateChange();
            }
          }

          if (!this.noIntercept) {
            start = new Date();
            if (this.addEventListener) {
              this.addEventListener("readystatechange", onReadyStateChange, false);
            } else {
              oldOnReadyStateChange = this.onreadystatechange;
              this.onreadystatechange = onReadyStateChange;
            }
          }
          send.call(this, data);
        };
      }
      // let source = rxjs.Node.fromEvent(XMLHttpRequest.prototype.send, 'send');
    }

    //Window Load Event Handler
    window.addEventListener("load", () => {
      addCustomEventListeners();
    });

    //Count number of elements
    // let paragraphCount = document.evaluate("count(//p)", document, null, XPathResult.ANY_TYPE, null);
    // console.info("This document contains " + paragraphCount.numberValue + " paragraph elements");

    //Get Xpath and XY Coordinates of any clicked element
    document.onclick = (event) => {
      // event.stopImmediatePropagation();
      if (event === undefined) event = window.event;                     // IE hack
      let target = "target" in event ? event.target : event.srcElement; // another IE hack
      let root = document.compatMode === "CSS1Compat" ? document.documentElement : document.body;
      let mxy = [event.clientX + root.scrollLeft, event.clientY + root.scrollTop];
      let path = getPathTo(target);
      if (path.includes("requestsBox") || path.includes("svgContainer") || path.includes("containerErrors") || path.includes("requestId-") || path.includes("replDiv")) return;//We don't want to acknowledge the click we do in our own terminal
      // event.preventDefault();
      masterId++;
      let txy = getPageXY(target);
      console.info(masterId + " - " + path + " <br/>offset x:" + (mxy[0] - txy[0]) + ", y:" + (mxy[1] - txy[1]));
      addCustomEventListeners();
      getDataFromWindow();
    };

    function getPathTo(element) {
      if (element.id !== "")
        return "id(\"" + element.id + "\")";
      if (element === document.body)
        return element.tagName;
      let ix = 0;
      if (!element.parentNode) {
        return element.tagName;
      }
      let siblings = element.parentNode.childNodes;
      for (let i = 0; i < siblings.length; i++) {
        let sibling = siblings[i];
        if (sibling === element)
          return getPathTo(element.parentNode) + "/" + element.tagName + "[" + (ix + 1) + "]";
        if (sibling.nodeType === 1 && sibling.tagName === element.tagName)
          ix++;
      }
    }

    function getPageXY(element) {
      let x = 0, y = 0;
      while (element) {
        x += element.offsetLeft;
        y += element.offsetTop;
        element = element.offsetParent;
      }
      return [x, y];
    }

    // Check if canonical link URI is matching window.location.href
    function checkCanonicalLink(id) {
      if (isIframe) {// We don't want to check this if we are in an Iframe to avoid false positive
        canonicalLinkURI = $("link[rel='canonical']");
        if (canonicalLinkURI[0] && canonicalLinkURI[0].href) {
          canonicalLinkURI = new URL(canonicalLinkURI[0].href);
          windowURI = new URL(window.location.href);
          const message = `${id} - Canonical: ${canonicalLinkURI}`;
          (canonicalLinkURI.pathname === windowURI.pathname) ?
            console.info(message) : console.error(message);
        } else {
          console.warn("No canonical link found");
        }
      }
    }

    checkCanonicalLink(masterId);

    // Evaluate the ENV variables
    function getEnvsFromWindow(id) {
      const env = window.eval("window.ENV");
      if (env) {
        console.info({ id: id, "ENV": env });
      }
    }

    // Evaluate the GTM DataLayer
    function getDataFromWindow() {
      const dataLayer = unsafeWindow.dataLayer;//This the best and simpler way to do it, eval gives random behaviour and code is non-debuggeable
      if (dataLayer) {
        let sameObject = false;
        if (dataLayer.length > 0) {
          sameObject = JSON.stringify(dataLayer[dataLayer.length - 1]) === JSON.stringify(cache[cache.length - 1]);
        }
        if ((!sameObject && dataLayer) || firstRun || dataLayer.length >= cache.length) {//If it's the first time render the dataLayer, after that, only render if you detect changes in dataLayer
          firstRun = false;
          cache.push(dataLayer[dataLayer.length - 1]);
          // console.info({ "GTM": dataLayer });
          //Observe GTM dataLayer
          const gtmObserver = rxjs.of(dataLayer);
          gtmObserver
            .subscribe(changedDataLayerEntry => {
                if (changedDataLayerEntry) {
                  console.info({ id: masterId, GTM: changedDataLayerEntry });
                } else {
                  console.warn("Subscribe null received");
                }
              },
              err => {
                console.error(err);
              },
              () => {
                console.info("GTM done");
              });
        }
      }
    }

    //Access window.dataLayer without skipping the Tamper Monkey Sandbox(secure method)
    setTimeout(() => {
      getDataFromWindow(masterId);
    }, 2000);
    getEnvsFromWindow(masterId);
    // try {
    //   unsafeWindow.onYouTubeIframeAPIReady = function() {
    //     alert("API loaded");
    //   };
    // } catch (e) {
    //   console.error({response: "unsafeWindowNotFound"})
    // }
    //Only way to access window.dataLayer is by creating a script in the DOM as seeing in https://programming.mediatagtw.com/article/tampermonkey+unsafewindow AND use // @grant        unsafeWindow
    // let script = document.createElement("script");
    // script.textContent = "(" + observeDataLayer.toString() + ")();";
    // try {
    //   document.head.appendChild(script);
    // } catch (e) {// This is for: Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'unsafe-eval' 'self' 'sha256-nnRzvGsB15enSSxWufoVP+C4WOA6Spq28ybk2OobhJo=' https://static.observablehq.com https://www.google-analytics.com https://www.googleapis.com https://apis.google.com https://js.stripe.com". Either the 'unsafe-inline' keyword, a hash ('sha256-NW48ymmYcooO0dbY1vr0sH+pipddsZUK7g5L8N3COw8='), or a nonce ('nonce-...') is required to enable inline execution.
    //   console.error(e);
    // }
    //REPL
    async function repl() {
      // let result = "input javascript code";
      // const expression = prompt(result);
      const command = $(".replDiv input").val();
      let result = await eval(command);
      if (result) {
        console.info({ response: result });
      }
      await sleep(0);
    }

    function xmlHttpRequestPromise(url, option) {
      return new Promise(resolve => {
        const newOption = { url, onload, ...option };
        if (!newOption.method) newOption.method = "GET";
        GM.xmlHttpRequest(newOption);

        function onload(response) {
          resolve(response);
        }
      });
    }

    function sleep(second) {
      return new Promise(wake => setTimeout(wake, second * 1000));
    }

    repl();

    //Get Event Url
    function getEventUrl(event) {
      try {
        return new URL(event.url || event.config.url);
      } catch (e) {//Sometimes event.url will not have `origin` attribute
        return event.url || undefined;
      }
    }

    // Get Json Tree Element from Event
    function getEventJsonTreeElement(type, event) {
      let jsonTreeElement, parsedResponse;
      try {
        if (type === GTM || type === ENV) {
          jsonTreeElement = renderjson(event[type]);
        } else {
          if (event.response) {
            parsedResponse = JSON.parse(event.response);
            jsonTreeElement = renderjson(parsedResponse);
          }
        }
      } catch (e) {
        // console.log(e); //Sometimes we get malformed JSON
        try {
          parsedResponse = JSON.parse(JSON.stringify({ response: event.response }));
          jsonTreeElement = renderjson(parsedResponse);
        } catch (e) {
          console.log(e);
        }
      }
      return jsonTreeElement;
    }

    // Add Json tree to alert message line
    function addJsonTreeToRequestLine(url, event, requestLine, type, jsonTreeElement) {
      if (url) {
        url = event.url ? event.url.replace(url.origin, "") : url.href;
        requestLine.append("<p class=\"objectMessageP\">" + event.id + " - " + (event.method ? event.method.toUpperCase() : "") + " " + event.statusCode + " " + event.duration + "<br/>" + "<a href=\"" + url + "\">" + url.substring(1, url.length) + "<a/><p\>"); // adding the error response to the message
      } else {
        if (type === GTM || type === ENV) {
          if (type === GTM && cache.length === 0) {
            cache.push(event.GTM[event.GTM.length - 1]);
          }
          requestLine.append("<p class=\"objectMessageP\">" + event.id + " - " + type + ":<p\>"); // adding the GTM info response to the message
        } else {
          requestLine.append("<p class=\"objectMessageP\">" + event.id + " - " + event[0] + "<p\>"); // adding the error response to the message
        }
      }
      if (jsonTreeElement) {
        requestLine.append(jsonTreeElement);
      }
      if (type === GTM) {
        requestLine.attr("data-object", JSON.stringify(event[type]));
        // alertLine.data("gtm-object", JSON.stringify(event[type]));
      }
    }

    //This method needs complex logic since every event needs to be rendered differently in the DOM(event, error, info, env_variable, GTM object), TODO NEEDS REFACTOR
    function appendObjectToRequestLine(event, requestLine, type = "event") {
      let url, jsonTreeElement;
      url = getEventUrl(event);
      jsonTreeElement = getEventJsonTreeElement(type, event);
      addJsonTreeToRequestLine(url, event, requestLine, type, jsonTreeElement);
    }

    //Render events into HTML
    function renderEventInHTML(event, alertType) {
      alertType = bubbleStates.includes(alertType) ? alertType : "primary";
      const newId = id++;
      const requestLine = $("<div id=\"requestId-" + newId + "\" class=\"alert alert-dismissible fade show\" style=\"display: none;\">");
      // const responseLine = $("<div id=\"responseId-" + newId + "\" style=\"display: block;\">");
      requestLine.addClass("alert-" + alertType);
      const close = $("<button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\">&times</button>");
      requestLine.append(close);
      if (typeof event === "string") {
        requestLine.append(event);
      } else {
        if (event.GTM || event.ENV) {
          appendObjectToRequestLine(event, requestLine, event.ENV ? ENV : GTM);
        } else {
          appendObjectToRequestLine(event, requestLine);
        }
      }
      requestLine.prependTo(containerErrors).fadeIn(300); //.delay(20000).fadeOut(500); //.delay(5000).fadeOut(500);
    }
  }
)();