[DEBUG] 显式日志

用 alert() 提示符合匹配规则的日志或未捕获异常,帮助开发者在日常使用网页时发现潜藏问题

当前为 2021-07-17 提交的版本,查看 最新版本

// ==UserScript==
// @name            [DEBUG] 显式日志
// @version         1.0.0.20210717
// @namespace       laster2800
// @author          Laster2800
// @description     用 alert() 提示符合匹配规则的日志或未捕获异常,帮助开发者在日常使用网页时发现潜藏问题
// @homepage        https://greasyfork.org/zh-CN/scripts/429521
// @supportURL      https://greasyfork.org/zh-CN/scripts/429521/feedback
// @license         LGPL-3.0
// @include         *
// @grant           GM_registerMenuCommand
// @grant           GM_setValue
// @grant           GM_getValue
// @grant           unsafeWindow
// @run-at          document-start
// @incompatible    firefox 完全不兼容 Greasemonkey,不完全兼容 Violentmonkey
// ==/UserScript==

(function() {
  'use strict'
  try {
    const gmInclude = GM_getValue('include')
    const gmExclude = GM_getValue('exclude')
    let include = gmInclude ? new RegExp(gmInclude) : null
    let exclude = gmExclude ? new RegExp(gmExclude) : null
    // 日志
    const console = unsafeWindow.console
    const logs = ['log', 'warn', 'error']
    for (const log of logs) {
      const _ = console[log]
      console[log] = function() {
        const m = [arguments, log]
        if (match(m, include) && !match(m, exclude)) {
          explicit(arguments.length == 1 ? arguments[0] : JSON.stringify(arguments), log.toUpperCase())
        }
        return _.apply(console, arguments)
      }
    }
    // 未捕获异常
    unsafeWindow.addEventListener('error', function(event) { // 正常
      const m = [event.error, event.filename, 'Uncaught Exception (Normal)']
      if (match(m, include) && !match(m, exclude)) {
        explicit(event.error, 'Uncaught Exception (Normal)')
      }
    })
    unsafeWindow.addEventListener('unhandledrejection', function(event) { // from Promise
      const m = [event.reason, 'Uncaught Exception (in Promise)']
      if (match(m, include) && !match(m, exclude)) {
        explicit(event.reason, 'Uncaught Exception (in Promise)')
      }
    })
    // 菜单
    GM_registerMenuCommand('设置过滤器', () => {
      try {
        const sInclude = prompt(`【${GM_info.script.name}】\n\n` + '设置匹配过滤器', include?.source ?? '.*')
        include = sInclude ? new RegExp(sInclude) : null
        GM_setValue('include', sInclude ?? '')
        const sExclude = prompt(`【${GM_info.script.name}】\n\n` + '设置排除过滤器', exclude?.source ?? '')
        exclude = sExclude ? new RegExp(sExclude) : null
        GM_setValue('exclude', sExclude ?? '')
      } catch (e) {
        explicit(e)
      }
    })
    GM_registerMenuCommand('说明', () => {
      alert(`【${GM_info.script.name}】\n\n` + '规则\n\n* 正则匹配,不必考虑转义。\n\n* 日志:可用 `LOG` / `WARN` / `ERROR` 作为匹配目标。如用 `^LOG$` 作为排除过滤器排除所有 INFO 级别日志。\n* 日志:无法监听到非直接通过 `console` 对象打印出来的日志,如在油猴黑箱中运行的脚本打印出来的日志。\n\n* 未捕获异常(正常):可用 `Uncaught Exception (Normal)` 作为匹配目标。如简单地用 `cau` 来过滤出所有未捕获异常,但可能混杂带 `cau` 信息的日志。\n* 未捕获异常(正常):可用异常抛出处的文件名作为匹配目标。\n\n* 未捕获异常(Promise):可用 `Uncaught Exception (in Promise)` 作为匹配目标。')
    })
  } catch (e) {
    explicit(e)
  }
})()

/**
 * 显式地显示信息
 * @param {*} msg 信息
 * @param {string} [label] 标记
 */
function explicit(msg, label) {
  alert(`【${GM_info.script.name}】${label ? `【${label}】` : ''}\n\n${msg}`)
}

/**
 * @param {*} obj 匹配对象
 * @param {RegExp} regex 匹配正则表达式
 * @param {number} [depth=5] 匹配查找深度
 * @returns {boolean} 是否匹配成功
 */
function match(obj, regex, depth = 5) {
  if (obj && regex && depth > 0) {
    return core(obj, depth, new Set())
  } else {
    return false
  }

  function core(obj, depth, objSet) {
    for (var key in obj) {
      if (regex.test(key)) {
        return true
      } else {
        try {
          var value = obj[key]
          if (value) {
            if (typeof value == 'object' || typeof value == 'function') {
              if (regex.test(value.toString())) {
                return true
              } else if (depth > 1) {
                if (!objSet.has(value)) {
                  objSet.add(value)
                  if (core(value, depth - 1)) {
                    return true
                  }
                }
              }
            } else {
              if (regex.test(String(value))) {
                return true
              }
            }
          }
        } catch (e) {
          // value that cannot be accessed
        }
      }
    }
    return false
  }
}