diff --git a/docs/Development-Plan.md b/docs/Development-Plan.md index a59cb47..c01e4eb 100644 --- a/docs/Development-Plan.md +++ b/docs/Development-Plan.md @@ -715,7 +715,7 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin - [ ] **9.1** Sentry-Cocoa SPM 依赖 + `Source/Analytics/SentryCrashReporter.swift` - 编译开关 `SENTRY_ENABLED`(默认 ON)+ `NoopCrashReporter` fallback - DSN 通过 xcconfig 注入,不入 git -- [ ] **9.2** `Source/Bridge/Handlers/H5ErrorRelay.swift`:注入 `window.onerror` / `unhandledrejection` polyfill + `reportH5Error` 桥 handler(Design §11.4,新增 handler 不破坏契约) +- [x] **9.2** `Source/Bridge/H5ErrorRelay.swift`:WKUserScript .atDocumentStart 注入 hook,拦截 `console.error` / `window.onerror` / `unhandledrejection`,通过独立的 `webkit.messageHandlers.h5error` 桥(与 WVJB 不同 channel)转发到原生 print。仅 BridgedWebView 安装(大厅+子游戏),Overlay 第三方外链不挂。零修改 H5。 - [ ] **9.3** 极光(JAnalytics):保持 `NoopAnalytics` stub(**ADR-005 决策**) - `Source/Analytics/Tracker.swift` 协议 + `NoopAnalytics` 实现 - `JAnalyticsTracker.swift` 留蓝图骨架(`#if JANALYTICS_ENABLED` 包裹,默认 OFF) @@ -972,7 +972,7 @@ H5 调 `OpenurlTitleData` 打开弹层 WebView,弹层内 H5 用 `window.settin ### Phase 9 SDK 真实化 + 监控 - [ ] 9.1 Sentry SPM + CrashReporter -- [ ] 9.2 H5ErrorRelay +- [x] 9.2 H5ErrorRelay(独立 webkit.messageHandlers.h5error,BridgedWebView 安装,Overlay 不挂) - [ ] 9.3 极光保持 NoopAnalytics(JANALYTICS_ENABLED OFF) - [ ] 9.4 闲聊保持 NoopSharePlatform - [ ] 9.5 Agora 保持 NoopVideoRoom(AGORA_ENABLED OFF) diff --git a/ylgamehall/Source/Bridge/H5ErrorRelay.swift b/ylgamehall/Source/Bridge/H5ErrorRelay.swift new file mode 100644 index 0000000..0624363 --- /dev/null +++ b/ylgamehall/Source/Bridge/H5ErrorRelay.swift @@ -0,0 +1,122 @@ +// +// H5ErrorRelay.swift +// ylgamehall +// +// H5 错误中继:把 WebView 内 `console.error` / `window.onerror` / +// `unhandledrejection` 通过 `webkit.messageHandlers.h5error` 上报到原生 print。 +// +// Phase 9.2(Design §11.2)。msext 时代仅靠用户/运营手工反馈;本项目作为 +// 原则 B「内部自由现代化」的一部分,开发期把 H5 异常打到 Xcode console, +// 减少调试盲点。生产期无开关控制(输出 print 默认走 OSLog,对体积影响极小)。 +// +// **不修改 H5 一行**(契约原则 A):用 WKUserScript `.atDocumentStart` 在 +// 业务 JS 跑前就 hook,不要求 H5 团队配合。 +// +// 仅在 BridgedWebView(大厅 + 子游戏)注入;OverlayViewController 是 +// 第三方外链,错误不属于 ylgamehall 内部信号,故不挂。 +// + +import Foundation +import WebKit + +@MainActor +public final class H5ErrorRelay: NSObject { + + public static let shared = H5ErrorRelay() + + /// WKScriptMessageHandler 名字。与下面 JS source 内 `webkit.messageHandlers.h5error` 必须一致。 + public static let messageName = "h5error" + + public override init() { super.init() } + + /// 把 hook JS + message handler 注册到给定的 UserContentController。 + /// BridgedWebView.init 在挂 WVJB 之后调一次。 + public func install(into controller: WKUserContentController) { + let userScript = WKUserScript( + source: Self.javaScriptSource, + injectionTime: .atDocumentStart, // 业务 JS 之前 hook + forMainFrameOnly: true // iframe 不抓 + ) + controller.addUserScript(userScript) + controller.add(MessageProxy(target: self), name: Self.messageName) + } + + fileprivate func handle(_ body: Any) { + guard let dict = body as? [String: Any], + let kind = dict["kind"] as? String else { return } + switch kind { + case "console.error": + let args = (dict["args"] as? [String]) ?? [] + print("[H5 console.error]", args.joined(separator: " ")) + case "onerror": + let msg = (dict["msg"] as? String) ?? "" + let src = (dict["src"] as? String) ?? "" + let line = (dict["line"] as? Int) ?? 0 + let col = (dict["col"] as? Int) ?? 0 + let stack = dict["stack"] as? String + print("[H5 onerror] \(msg) at \(src):\(line):\(col)" + (stack.map { "\n\($0)" } ?? "")) + case "unhandledrejection": + let reason = (dict["reason"] as? String) ?? "" + let stack = dict["stack"] as? String + print("[H5 unhandledrejection]", reason + (stack.map { "\n\($0)" } ?? "")) + default: + print("[H5 unknown error]", dict) + } + } + + /// JS 端 hook 三类异常源,统一通过 `webkit.messageHandlers.h5error.postMessage(payload)` + /// 上报。所有 send 调用都包 try/catch,hook 自身永远不应抛错(避免污染业务流程)。 + private static let javaScriptSource = """ + (function(){ + function send(payload){ + try { window.webkit.messageHandlers.h5error.postMessage(payload); } catch(e){} + } + var origErr = console.error; + console.error = function(){ + try { + var args = []; + for (var i=0; i