[DEBUG] 信息显式化

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

当前为 2022-08-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name [DEBUG] 信息显式化
  3. // @version 2.8.1.20220813
  4. // @namespace laster2800
  5. // @author Laster2800
  6. // @description 用 alert() 提示符合匹配规则的日志或未捕获异常,帮助开发者在日常使用网页时发现潜藏问题
  7. // @homepageURL https://greasyfork.org/zh-CN/scripts/429521
  8. // @supportURL https://greasyfork.org/zh-CN/scripts/429521/feedback
  9. // @license LGPL-3.0
  10. // @include *
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_unregisterMenuCommand
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @run-at document-start
  16. // @compatible edge 版本不小于 85
  17. // @compatible chrome 版本不小于 85
  18. // @compatible firefox 版本不小于 90
  19. // ==/UserScript==
  20.  
  21. (function() {
  22. 'use strict'
  23.  
  24. const errorLog = console.error
  25. const win = typeof unsafeWindow === 'object' ? unsafeWindow : window
  26.  
  27. const gm = {
  28. id: 'gm429521',
  29. injectUpdate: 20220813,
  30. config: {},
  31. fn: {
  32. /**
  33. * 获取封装日志函数
  34. * @param {Object} console 控制台对象
  35. * @param {Function} log 日志函数
  36. * @param {string} type 类型
  37. * @param {string} [source] 源
  38. * @returns {Function} 封装日志函数
  39. */
  40. wrappedLog(console, log, type, source) {
  41. const { config, fn } = gm
  42. return (...args) => {
  43. Reflect.apply(log, console, args)
  44. try {
  45. if (config.enabled) {
  46. const m = [args, type]
  47. if (fn.match(m, config.include) && !fn.match(m, config.exclude)) {
  48. let msg = null
  49. if (args.length === 1) {
  50. if (args[0] && typeof args[0] === 'object') {
  51. msg = JSON.stringify(args[0], null, 2)
  52. } else {
  53. msg = args[0]
  54. }
  55. } else {
  56. msg = JSON.stringify(args, null, 2)
  57. }
  58. fn.explicit(msg, type, source)
  59. }
  60. }
  61. } catch (e) {
  62. innerError(e)
  63. }
  64. }
  65. },
  66. /**
  67. * 显式地显示信息
  68. * @param {*} msg 信息
  69. * @param {string} [type] 类型
  70. * @param {string} [source] 源
  71. */
  72. explicit(msg, type, source) {
  73. alert(`${GM_info.script.name}${type ? `\nTYPE: ${type}` : ''}${source ? `\nSOURCE: ${source}` : ''}\n\n${msg}`)
  74. },
  75. /**
  76. * @param {*} obj 匹配对象
  77. * @param {RegExp} regex 匹配正则表达式
  78. * @param {number} [depth=5] 匹配查找深度
  79. * @returns {boolean} 是否匹配成功
  80. */
  81. match(obj, regex, depth = 5) {
  82. if (obj && regex && depth > 0) {
  83. return inner(obj, depth, new WeakSet())
  84. } else {
  85. return false
  86. }
  87.  
  88. function inner(obj, depth, objSet) {
  89. if (!obj) return false
  90. innerLoop: for (const key in obj) {
  91. if (regex.test(key)) {
  92. return true
  93. } else {
  94. try {
  95. const value = obj[key]
  96. if (value && (typeof value === 'object' || typeof value === 'function')) {
  97. if (value === obj) continue
  98. if (value === value.window) continue // exclude Window
  99. for (const type of [Function, Node, StyleSheet]) {
  100. if (value instanceof type) continue innerLoop
  101. }
  102.  
  103. if (regex.test(value.toString())) {
  104. return true
  105. } else if (depth > 1) {
  106. if (!objSet.has(value)) {
  107. objSet.add(value)
  108. if (inner(value, depth - 1, objSet)) {
  109. return true
  110. }
  111. }
  112. }
  113. } else if (regex.test(String(value))) {
  114. return true
  115. }
  116. } catch { /* value that cannot be accessed */ }
  117. }
  118. }
  119. return false
  120. }
  121. },
  122. /**
  123. * 检查更新
  124. * @param {string} source 源
  125. * @param {boolean} [injectAhead] 注入版版本超前
  126. */
  127. updateCheck(source, injectAhead) {
  128. if (injectAhead) {
  129. this.explicit(`「[DEBUG] 信息显式化」版本落后于「${source}」中的注入版。请在稍后弹出的新标签页中获取最新版主脚本。\n若弹出页被浏览器阻止,请手动查看浏览器的「已阻止弹出窗口」,前往主脚本主页进行更新。`, 'UPDATE', source)
  130. window.open('https://greasyfork.org/zh-CN/scripts/429521')
  131. } else {
  132. this.explicit(`需要更新「[DEBUG] 信息显式化(注入版)」。请在稍后弹出的新标签页中获取最新版 URL 并更新「${source}」中的「@require」属性值。\n若弹出页被浏览器阻止,请手动查看浏览器的「已阻止弹出窗口」,前往注入版主页进行更新。`, 'UPDATE', source)
  133. window.open('https://greasyfork.org/zh-CN/scripts/429525')
  134. }
  135. },
  136. },
  137. }
  138. win[Symbol.for('ExplicitMessage')] = gm
  139.  
  140. try {
  141. // 配置
  142. const df = { include: '.*', exclude: '^LOG$' }
  143. const gmInclude = GM_getValue('include') ?? df.include
  144. const gmExclude = GM_getValue('exclude') ?? df.exclude
  145. gm.config.enabled = GM_getValue('enabled') ?? true
  146. gm.config.include = gmInclude ? new RegExp(gmInclude) : null
  147. gm.config.exclude = gmExclude ? new RegExp(gmExclude) : null
  148.  
  149. // 日志
  150. const { console } = win
  151. for (const n of ['log', 'debug', 'info', 'warn', 'error']) {
  152. console[n] = gm.fn.wrappedLog(console, console[n], n.toUpperCase())
  153. }
  154.  
  155. // 未捕获异常
  156. win.addEventListener('error', /** @param {ErrorEvent} event */ event => { // 常规
  157. try {
  158. if (!gm.config.enabled) return
  159. const message = event.error?.stack ?? event.message
  160. const m = [message, event.filename, 'Uncaught Exception (Normal)']
  161. if (gm.fn.match(m, gm.config.include) && !gm.fn.match(m, gm.config.exclude)) {
  162. gm.fn.explicit(message, 'Uncaught Exception (Normal)')
  163. }
  164. } catch (e) {
  165. innerError(e)
  166. }
  167. })
  168. win.addEventListener('unhandledrejection', /** @param {PromiseRejectionEvent} event */ event => { // Promise
  169. try {
  170. if (!gm.config.enabled) return
  171. const message = event.reason.stack ?? event.reason
  172. const m = [message, 'Uncaught Exception (in Promise)']
  173. if (gm.fn.match(m, gm.config.include) && !gm.fn.match(m, gm.config.exclude)) {
  174. gm.fn.explicit(message, 'Uncaught Exception (in Promise)')
  175. }
  176. } catch (e) {
  177. innerError(e)
  178. }
  179. })
  180.  
  181. // 菜单
  182. if (self === top) { // frame 中不要执行
  183. const initScriptMenu = () => {
  184. const menuMap = {}
  185. menuMap.enabled = GM_registerMenuCommand(`当前${gm.config.enabled ? '开启' : '关闭'}`, () => {
  186. try {
  187. gm.config.enabled = confirm(`${GM_info.script.name}\n\n「确定」以开启功能,「取消」以关闭功能。`)
  188. GM_setValue('enabled', gm.config.enabled)
  189. for (const menuId of Object.values(menuMap)) {
  190. GM_unregisterMenuCommand(menuId)
  191. }
  192. initScriptMenu()
  193. } catch (e) {
  194. innerError(e)
  195. }
  196. })
  197. menuMap.filter = GM_registerMenuCommand('设置过滤器', () => {
  198. try {
  199. const sInclude = prompt(`${GM_info.script.name}\n\n设置匹配过滤器:`, gm.config.include?.source ?? df.include)
  200. if (typeof sInclude === 'string') {
  201. gm.config.include = sInclude ? new RegExp(sInclude) : null
  202. GM_setValue('include', sInclude)
  203. }
  204. const sExclude = prompt(`${GM_info.script.name}\n\n设置排除过滤器:`, gm.config.exclude?.source ?? df.exclude)
  205. if (typeof sExclude === 'string') {
  206. gm.config.exclude = sExclude ? new RegExp(sExclude) : null
  207. GM_setValue('exclude', sExclude)
  208. }
  209. } catch (e) {
  210. innerError(e)
  211. }
  212. })
  213. menuMap.help = GM_registerMenuCommand('使用说明', () => window.open('https://gitee.com/liangjiancang/userscript/blob/master/script/ExplicitMessage/README.md#使用说明'))
  214. menuMap.inject = GM_registerMenuCommand('获取注入版', () => window.open('https://greasyfork.org/zh-CN/scripts/429525'))
  215. }
  216. initScriptMenu()
  217. }
  218. } catch (e) {
  219. innerError(e)
  220. }
  221.  
  222. /**
  223. * 内部错误
  224. * @param {*} e 错误
  225. */
  226. function innerError(e) {
  227. gm.fn.explicit(e, 'UNKNOWN', GM_info.script.name)
  228. errorLog('[UNKNOWN ERROR] %s: %o', GM_info.script.name, e)
  229. }
  230. })()