Bubble Logger

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

当前为 2022-03-04 提交的版本,查看 最新版本

// ==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
// @namespace    http://tampermonkey.net/
// @version      1.0
// @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/
//IIFE
(function() {
    "use strict";
    let id = 0;
    const isIFrame = (window.self === window.top);
    const $ = window.jQuery;
    const rxjs = window.rxjs;
    window.dataLayer = window.dataLayer || [];
    const spinner = $("<div class=\"svgDiv\"><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 bubbleStates = ["primary", "danger", "warning"];
    console.defaultError = console.error.bind(console);
    console.errors = [];
    console.defaultWarn = console.warn.bind(console);
    console.warns = [];
    console.defaultInfo = console.info.bind(console);
    console.infos = [];

    importCSS();

    const requestsBox = $("<div id=\"requestsBox\" 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\" role=\"button\">href</a>");
    const refreshButton = $("<a class=\"btn btn-primary\" onclick=\"window.location.reload(true)\" role=\"button\">refresh</a>");
    const closeButton = $("<a class=\"btn btn-danger\" role=\"button\">X</a>");
    const buttonsDiv = $("<div class=\"buttonsDiv\"></div>");
    refreshButton.prependTo(buttonsDiv);
    hrefButton.prependTo(buttonsDiv);
    closeButton.prependTo(buttonsDiv);

    spinner.prependTo(containerSvg);
    buttonsDiv.appendTo(containerSvg);
    containerSvg.prependTo(requestsBox);
    requestsBox.appendTo($("body"));
    containerErrors.appendTo($("body"));

    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);
      // $(containerErrors).css("display", "none");
      setTimeout(() => {
        if (closeButtons) {
          $(requestsBox).addClass("customCollapse");
          $(".svgContainer svg").css("float", "right");
        }
        $(".containerErrors div").fadeOut("2000");
        if (collapseSpinner) {
          $(buttonsDiv).css("display", "none");
          $(containerErrors).css("display", "none");
          $(containerSvg).css("left", "1%");
          if (isIFrame) {
            $(requestsBox).css("left", "-37%");
          } else {
            $(requestsBox).css("left", "-30%");
          }
        }
      }, 1000);
    }

    spinner.on("click", function() {
      if ($(requestsBox).hasClass("customCollapse")) {
        $(containerErrors).css("display", "block");
        $(requestsBox).removeClass("customCollapse");
        $(".containerErrors div").fadeIn("2000");
        $(".svgContainer svg").css("float", "left");
        $(requestsBox).css("left", "0%");
        $(buttonsDiv).css("display", "block");
        $(containerErrors).css("display", "block");
      } else {
        closeContainerErrorsWithAnimations(true, true);
      }
      // $(containerErrors).siblings().hide("slow");
    });

    hrefButton.on("click", function() {
      const anchors = $("a[href]").not(".svgContainer a").not(".containerErrors a");
      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");
        // }
      });
      $(anchors).css("background", "red").css("border-color", "yellow").css("border", "2px");
    });
    closeButton.on("click", function() {
      // $(".containerErrors div").fadeOut("2000");
      closeContainerErrorsWithAnimations();
    });

    // responsesBox.appendTo($("body"));

    function importCSS() {
      // 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;
                                                    }`;
      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; }`;
      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 requestsBoxCssTop = (isIFrame) ? "-37%" : "-30%";
      const requestsBoxCss = `.requestsBox {
                                overflow-y: scroll !important;
                                max-height: 88% !important;
                                opacity: 0.95 !important;
                                position: fixed !important;
                                top: 0% !important;
                                width: 40% !important;
                                z-index: 99999999
                                !important;
                                left: ${requestsBoxCssTop};
                              } .string {word-wrap: break-word !important;}`;
      GM_addStyle(requestsBoxCss);
      const svgDivCss = ".svgDiv {width: 100% !important; float: left; background-color: black !important} .svgContainer svg {float: right}";
      GM_addStyle(svgDivCss);
      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% !important" : "25.5% !important";
      const containerErrorsCss = `.containerErrors {
                                      display: none;
                                      max-height: 752px !important;
                                      width: 100% !important;
                                      top: ${top};
                                      width: 40% !important;
                                      position: fixed !important;
                                      overflow-y: scroll !important;
                                      z-index: 99999999 !important;
                                   }`;
      GM_addStyle(containerErrorsCss);
      const containerSvgCss = ".svgContainer {cursor: pointer !important;left: 1%; z-index: 99999999 !important;} .svgContainer svg{float: right}";
      GM_addStyle(containerSvgCss);
    }

    function appendObjectToHTML(event, requestLine) {
      let url;
      try {
        url = new URL(event.url);
      } catch (e) {//Sometimes event.url will not have origin
        url = event.url;
      }
      let jsonHTML;
      let response;
      try {
        response = JSON.parse(event.response);
      } catch (e) {
        console.log(e);//Sometimes we get malformed jsons
        response = JSON.parse(JSON.stringify({ response: event.response }));
      }
      jsonHTML = renderjson(response);
      if (event.url) {
        url = event.url.replace(url.origin, "");
        requestLine.append("<p style=\"word-break: break-word;\">" + " <a href=\"" + event.url + "\" class=\"alert-link\">" + url.substring(1, url.length) + "<a/> <br/>" + (event.method ? event.method.toUpperCase() : "") + " " + event.statusCode + " " + event.duration + "<p\>"); // adding the error response to the message
      } else {
        requestLine.append("<p style=\"word-break: break-word;\">" + event[0] + "<p\>"); // adding the error response to the message
      }
      // if($(requestsBox).hasClass("customCollapse")){
      //   requestLine.css("display", "none");
      // }
      requestLine.append(jsonHTML);
    }

    function bubbleErrorInHtml(event, bubbleType) {
      bubbleType = bubbleStates.includes(bubbleType) ? bubbleType : "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-" + bubbleType);
      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 {
        appendObjectToHTML(event, requestLine);
      }
      requestLine.prependTo(containerErrors).fadeIn(300); //.delay(20000).fadeOut(500); //.delay(5000).fadeOut(500);
    }

    console.error = function() {
      bubbleErrorInHtml(arguments[0], "danger");
      // default &  console.error()
      console.defaultError.apply(console, arguments);
      // new & array data
      console.errors.push(Array.from(arguments));
    };

    console.warn = function() {
      bubbleErrorInHtml(arguments[0], "warning");
      // default &  console.error()
      console.defaultWarn.apply(console, arguments);
      // new & array data
      console.warns.push(Array.from(arguments));
    };

    console.info = function() {
      bubbleErrorInHtml(arguments[0], "primary");
      // default &  console.error()
      console.defaultInfo.apply(console, arguments);
      // new & array data
      console.infos.push(Array.from(arguments));
    };

    window.addEventListener("load", function() {
      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 = [
              "Message: " + eventMessage,
              "URL: " + url,
              "Line: " + lineNo,
              "Column: " + columnNo,
              "Error object: " + JSON.stringify(error)
            ].join(" - ");
            console.error(message);
          }
          //GM_notification('text', 'title', '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', ()=>{console.log('click');})
          return false;
        };
        // Handle Uncaught Errors
        window.onerror = function() {
          let args = Array.prototype.slice.call(arguments);
          if (_onerror) {
            return _onerror.apply(window, args);
          }
          return false;
        };
      }
      window.addEventListener("unhandledrejection", function(errorEvent) {
        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.rejectionhandled: " + error);
      });
      window.addEventListener("rejectionhandled", function(errorEvent) {
        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.rejectionhandled: " + error);
      });
      window.addEventListener("error", function(errorEvent) {
        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.error: " + error);
      });
      window.addEventListener("fetch", function(event) {
        let error = undefined;
        try {
          if (event.reason) {
            error = event.reason.stack || event.reason.message;
          } else {
            error = event.error || JSON.stringify(event);
          }
        } catch (e) {
          console.log(e);
        }
        console.warn("window.fetch: " + error);
        event.respondWith(
          fetch(event.request)
        );
      });
      $.error = function(message) {
        console.error("jQuery: " + message);
      };

      let stats = [];
      let timeoutId = null;
      let open = XMLHttpRequest.prototype.open;
      let send = XMLHttpRequest.prototype.send;

      XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
        this._url = url;
        open.call(this, method, url, async, user, pass);
      };

      function sendToConsole(event, 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);
          }
        });
      }

      XMLHttpRequest.prototype.send = function(data) {
        let self = this;
        let start;
        let oldOnReadyStateChange;
        let url = this._url;

        function onReadyStateChange(event) {
          //Info: Log all you need from event
          if (self.readyState === 4 /* complete */) {
            let time = new Date() - start;
            stats.push({
              url: url,
              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 : "unknown"
            });

            if (!timeoutId) {
              timeoutId = window.setTimeout(function() {
                sendToConsole(event, stats);
                timeoutId = null;
                stats = [];
              }, 2000);
            }
          }
          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);
      };
    });

    //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
    function observeDataLayer() {
      for (let i = 0; i < 10; i++) {
        console.log(window.dataLayer);
        console.log(window.jQuery);
      }
      rxjs.of(window.dataLayer)
        .subscribe(x => {
            try {
              const element = document.createElement("div");
              element.innerText = "Data: " + JSON.stringify(x);
              document.body.appendChild(element);
              document.body.appendChild(JSON.stringify(window.dataLayer));
            } catch (e) {
              console.log(e);
            }
          },
          err => {
            console.log(err);
          },
          () => {
            const element = document.createElement("div");
            element.innerText = "All done";
            document.body.appendChild(element);
          });
    }

    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);
    }
    //Get info of clicked elements
    document.onclick = (event) => {
      if (event === undefined) event = window.event;                     // IE hack
      let target = "target" in event ? event.target : event.srcElement; // another IE hack
      // if (window.event.path[0].attributes[0].nodeValue ==='requestsBox' || window.event.path[2].attributes[0].nodeValue) return;//Don't
      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-")) return;
      let txy = getPageXY(target);
      // path = path.replaceAll("/", "\n");
      //// let treePath = {response: {}};
      // const array = [1, 2, 3, 4];
      // const reducer = (accumulator, currentValue, index ) => {
      //   return accumulator + '<pre>' + currentValue;
      // }
      // 1 + 2 + 3 + 4
      // console.log(array.reduce(reducer)); //10
      // event.response = path.split("/").reduce(reducer)
      ////path.split("/").forEach(function(part, index) {
      // const last = index - 1;
      ////treePath.response.add(index, part)
      ////});
      ////treePath["offset"] = {
      ////x: (mxy[0] - txy[0]),
      ////y: (mxy[1] - txy[1])
      ////};
      ////console.info(treePath);
      console.info(path + " \noffset x:" + (mxy[0] - txy[0]) + ", y:" + (mxy[1] - txy[1]));
    };

    function getPathTo(element) {
      if (element.id !== "")
        return "id(\"" + element.id + "\")";
      if (element === document.body)
        return element.tagName;

      let ix = 0;
      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];
    }

    //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");
    // Check if canonical link URI is matching window.location.href
    let canonicalLinkURI, windowURI;
    try {
      setTimeout(() => {// This needs to be async and timedout to aovid false positives when website has a redirection
        if (window.self === window.top) {// 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 = `Canonical url: ${canonicalLinkURI}`;
            if (canonicalLinkURI.pathname === windowURI.pathname) {
              console.info(message);
            } else {
              console.error(message);
            }
          } else {
            console.warn("No canonical link found");
          }
        }
      }, 2000);
    } catch (e) {
      console.log(e);
      return;
    }
  }
)();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址