Bubble Logger

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

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==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);
    }
  }
)();