mirror of
https://github.com/dirtydishes/dreamio.git
synced 2026-06-06 21:38:15 +00:00
add webview inspection diagnostics
This commit is contained in:
parent
f22df976e4
commit
1563a4c067
3 changed files with 509 additions and 0 deletions
|
|
@ -4,6 +4,7 @@ import WebKit
|
|||
final class DreamioWebViewController: UIViewController {
|
||||
private enum Constants {
|
||||
static let stremioWebURL = URL(string: "https://web.stremio.com/")!
|
||||
static let diagnosticsMessageHandler = "dreamioDiagnostics"
|
||||
}
|
||||
|
||||
private lazy var webView: WKWebView = {
|
||||
|
|
@ -12,6 +13,13 @@ final class DreamioWebViewController: UIViewController {
|
|||
configuration.allowsInlineMediaPlayback = true
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
||||
#if DEBUG
|
||||
configuration.userContentController.add(
|
||||
WeakScriptMessageHandler(delegate: self),
|
||||
name: Constants.diagnosticsMessageHandler
|
||||
)
|
||||
configuration.userContentController.addUserScript(Self.playbackDiagnosticsScript)
|
||||
#endif
|
||||
|
||||
let webView = WKWebView(frame: .zero, configuration: configuration)
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
@ -20,6 +28,11 @@ final class DreamioWebViewController: UIViewController {
|
|||
webView.navigationDelegate = self
|
||||
webView.uiDelegate = self
|
||||
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
#if DEBUG
|
||||
if #available(iOS 16.4, *) {
|
||||
webView.isInspectable = true
|
||||
}
|
||||
#endif
|
||||
return webView
|
||||
}()
|
||||
|
||||
|
|
@ -32,6 +45,96 @@ final class DreamioWebViewController: UIViewController {
|
|||
|
||||
private var progressObservation: NSKeyValueObservation?
|
||||
|
||||
#if DEBUG
|
||||
private static let playbackDiagnosticsScript = WKUserScript(
|
||||
source: """
|
||||
(() => {
|
||||
if (window.__dreamioPlaybackDiagnosticsInstalled) {
|
||||
return;
|
||||
}
|
||||
window.__dreamioPlaybackDiagnosticsInstalled = true;
|
||||
|
||||
const post = (type, payload = {}) => {
|
||||
try {
|
||||
window.webkit.messageHandlers.dreamioDiagnostics.postMessage({
|
||||
type,
|
||||
payload,
|
||||
href: window.location.href
|
||||
});
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
const describeValue = (value) => {
|
||||
if (value instanceof Error) {
|
||||
return {
|
||||
name: value.name,
|
||||
message: value.message,
|
||||
stack: value.stack
|
||||
};
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch (_) {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
["error", "warn"].forEach((level) => {
|
||||
const original = console[level];
|
||||
console[level] = (...args) => {
|
||||
post(`console.${level}`, { args: args.map(describeValue) });
|
||||
original.apply(console, args);
|
||||
};
|
||||
});
|
||||
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
post("unhandledrejection", {
|
||||
reason: describeValue(event.reason)
|
||||
});
|
||||
});
|
||||
|
||||
const videoState = (video) => ({
|
||||
currentSrc: video.currentSrc || video.src || "",
|
||||
networkState: video.networkState,
|
||||
readyState: video.readyState,
|
||||
errorCode: video.error ? video.error.code : null,
|
||||
errorMessage: video.error ? video.error.message : null
|
||||
});
|
||||
|
||||
const attachVideoDiagnostics = (video) => {
|
||||
if (!video || video.__dreamioDiagnosticsAttached) {
|
||||
return;
|
||||
}
|
||||
video.__dreamioDiagnosticsAttached = true;
|
||||
video.addEventListener("error", () => {
|
||||
post("video.error", videoState(video));
|
||||
}, true);
|
||||
};
|
||||
|
||||
document.querySelectorAll("video").forEach(attachVideoDiagnostics);
|
||||
|
||||
new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node instanceof HTMLVideoElement) {
|
||||
attachVideoDiagnostics(node);
|
||||
}
|
||||
if (node.querySelectorAll) {
|
||||
node.querySelectorAll("video").forEach(attachVideoDiagnostics);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).observe(document.documentElement, { childList: true, subtree: true });
|
||||
})();
|
||||
""",
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: false
|
||||
)
|
||||
#endif
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
|
@ -77,6 +180,70 @@ final class DreamioWebViewController: UIViewController {
|
|||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private func logDiagnostic(type: String, payload: Any, pageURL: String?) {
|
||||
let redactedPageURL = pageURL.map(redactedURLString) ?? "unknown"
|
||||
let redactedPayload = redactDiagnosticValue(payload)
|
||||
print("[DreamioDiagnostics] \(type) page=\(redactedPageURL) payload=\(redactedPayload)")
|
||||
}
|
||||
|
||||
private func redactDiagnosticValue(_ value: Any) -> Any {
|
||||
switch value {
|
||||
case let string as String:
|
||||
return redactedURLString(string)
|
||||
case let array as [Any]:
|
||||
return array.map(redactDiagnosticValue)
|
||||
case let dictionary as [String: Any]:
|
||||
return dictionary.reduce(into: [String: Any]()) { result, entry in
|
||||
result[entry.key] = redactDiagnosticValue(entry.value)
|
||||
}
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
private func redactedURLString(_ value: String) -> String {
|
||||
guard var components = URLComponents(string: value), components.scheme != nil else {
|
||||
return redactTokenLikeFragments(in: value)
|
||||
}
|
||||
|
||||
components.query = nil
|
||||
components.fragment = nil
|
||||
if let path = components.percentEncodedPath.nilIfEmpty {
|
||||
components.percentEncodedPath = redactTokenLikePathSegments(in: path)
|
||||
}
|
||||
return redactTokenLikeFragments(in: components.string ?? value)
|
||||
}
|
||||
|
||||
private func redactTokenLikePathSegments(in path: String) -> String {
|
||||
path
|
||||
.split(separator: "/", omittingEmptySubsequences: false)
|
||||
.map { segment -> String in
|
||||
let text = String(segment)
|
||||
if text.range(of: #"^[A-Za-z0-9_-]{24,}$"#, options: .regularExpression) != nil {
|
||||
return "[redacted]"
|
||||
}
|
||||
return text
|
||||
}
|
||||
.joined(separator: "/")
|
||||
}
|
||||
|
||||
private func redactTokenLikeFragments(in value: String) -> String {
|
||||
let patterns = [
|
||||
#"(?i)((?:token|access_token|auth|signature|sig|key|apikey|api_key|jwt|session|password)=)([^&\s]+)"#,
|
||||
#"(?i)(bearer\s+)[A-Za-z0-9._~+/=-]+"#
|
||||
]
|
||||
|
||||
return patterns.reduce(value) { redacted, pattern in
|
||||
redacted.replacingOccurrences(
|
||||
of: pattern,
|
||||
with: "$1[redacted]",
|
||||
options: .regularExpression
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension DreamioWebViewController: WKNavigationDelegate {
|
||||
|
|
@ -99,11 +266,31 @@ extension DreamioWebViewController: WKNavigationDelegate {
|
|||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
decidePolicyFor navigationResponse: WKNavigationResponse,
|
||||
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void
|
||||
) {
|
||||
#if DEBUG
|
||||
if let response = navigationResponse.response as? HTTPURLResponse {
|
||||
let url = response.url?.absoluteString ?? "unknown"
|
||||
print("[DreamioNavigation] status=\(response.statusCode) url=\(redactedURLString(url))")
|
||||
}
|
||||
#endif
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
#if DEBUG
|
||||
print("[DreamioNavigation] didFail url=\(redactedURLString(webView.url?.absoluteString ?? "unknown")) error=\(redactedURLString(error.localizedDescription))")
|
||||
#endif
|
||||
showLoadFailure(error)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
#if DEBUG
|
||||
print("[DreamioNavigation] didFailProvisional url=\(redactedURLString(webView.url?.absoluteString ?? "unknown")) error=\(redactedURLString(error.localizedDescription))")
|
||||
#endif
|
||||
showLoadFailure(error)
|
||||
}
|
||||
|
||||
|
|
@ -124,6 +311,44 @@ extension DreamioWebViewController: WKNavigationDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
extension DreamioWebViewController: WKScriptMessageHandler {
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
guard message.name == Constants.diagnosticsMessageHandler,
|
||||
let body = message.body as? [String: Any],
|
||||
let type = body["type"] as? String
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
logDiagnostic(
|
||||
type: type,
|
||||
payload: body["payload"] ?? [:],
|
||||
pageURL: body["href"] as? String
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
weak var delegate: WKScriptMessageHandler?
|
||||
|
||||
init(delegate: WKScriptMessageHandler) {
|
||||
self.delegate = delegate
|
||||
super.init()
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
delegate?.userContentController(userContentController, didReceive: message)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var nilIfEmpty: String? {
|
||||
isEmpty ? nil : self
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension DreamioWebViewController: WKUIDelegate {
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue