From 439265d9c0e93ac8128a2c162c7fbeb2f211b251 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:29:18 +0000 Subject: [PATCH] Debug: forward embed logs to parent when embed_debug=1 This commit instruments the embed runtime to forward structured logs and lifecycle events (Nostr, relay, pointer resolution, playback decision, WebTorrent) to the parent window via `postMessage` when the `embed_debug=1` query parameter is present. It also adds a defensive guard to `resetTorrentStats` in `js/app.js` to prevent TypeErrors. Changes: - Added `js/embedDiagnostics.js` module. - Modified `js/embed.js` to initialize diagnostics and instrument startup. - Modified `js/app.js` to instrument playback fallback and patch `resetTorrentStats`. - Modified `js/webtorrent.js` to instrument peer probing and streaming. --- js/app.js | 22 ++++- js/embed.js | 106 ++++++++++++++---------- js/embedDiagnostics.js | 177 +++++++++++++++++++++++++++++++++++++++++ js/webtorrent.js | 10 ++- 4 files changed, 270 insertions(+), 45 deletions(-) create mode 100644 js/embedDiagnostics.js diff --git a/js/app.js b/js/app.js index 157adbb3..be021a6d 100644 --- a/js/app.js +++ b/js/app.js @@ -3,6 +3,7 @@ import { nostrClient } from "./nostrClientFacade.js"; import { recordVideoView } from "./nostrViewEventsFacade.js"; import { torrentClient } from "./webtorrent.js"; +import { emit } from "./embedDiagnostics.js"; import { isDevMode, ADMIN_SUPER_NPUB, @@ -4901,14 +4902,16 @@ class Application { } resetTorrentStats() { - if (this.videoModal) { - if (typeof this.videoModal.resetStats === "function") { + try { + if (this.videoModal && typeof this.videoModal.resetStats === "function") { this.videoModal.resetStats(); } else { devLogger.info( "[Application] resetTorrentStats: videoModal.resetStats not available — skipping." ); } + } catch (err) { + devLogger.warn("[Application] resetTorrentStats failed", err); } } @@ -8192,6 +8195,16 @@ class Application { */ async playVideoWithFallback(options = {}) { const { url = "", magnet = "", trigger, forcedSource } = options || {}; + + emit("playback-decision", { + method: forcedSource || (magnet ? "webtorrent" : "url"), // heuristic + details: { + url: Boolean(url), + magnet: Boolean(magnet), + forcedSource, + }, + }); + const hasTrigger = Object.prototype.hasOwnProperty.call( options || {}, "trigger" @@ -8459,6 +8472,11 @@ class Application { return result; } + emit("playback-started", { + method: result.source, + details: { startedAt: Date.now() }, + }); + return result; } diff --git a/js/embed.js b/js/embed.js index 9755e430..89d94309 100644 --- a/js/embed.js +++ b/js/embed.js @@ -5,45 +5,38 @@ import { resolveVideoPointer } from "./utils/videoPointer.js"; import { devLogger, userLogger } from "./utils/logger.js"; import { nostrToolsReady } from "./nostrToolsBootstrap.js"; -// Diagnostic forwarder for embed debugging -function postToParent(type, payload) { - try { - if (window.parent !== window) { - window.parent.postMessage({ __bitvid_debug: true, type, payload }, "*"); - } - } catch (e) { - /* no-op */ - } -} - -window.addEventListener("error", (ev) => { - postToParent("error", { - message: ev.message, - filename: ev.filename, - lineno: ev.lineno, - colno: ev.colno, - stack: ev.error && ev.error.stack, - }); -}); - -window.addEventListener("unhandledrejection", (ev) => { - postToParent("unhandledrejection", { - reason: (ev.reason && ev.reason.stack) || String(ev.reason), - }); -}); - -["log", "info", "warn", "error", "debug"].forEach((level) => { - if (console[level]) { - const orig = console[level].bind(console); - console[level] = (...args) => { - postToParent("console", { - level, - args: args.map((a) => (typeof a === "string" ? a : String(a))), - }); - orig(...args); - }; - } -}); +// early in embed bootstrap (insert before any heavy init) +const urlParams = new URLSearchParams(window.location.search); +const embedDebugEnabled = + urlParams.get("embed_debug") === "1" || urlParams.get("debug") === "embed"; +let diag = null; +const diagPromise = embedDebugEnabled + ? import("./embedDiagnostics.js") + .then((m) => { + diag = m.initEmbedDiagnostics({ enabled: true }); + // immediately signal startup + diag.emit("embed-start", { + url: location.href, + userAgent: navigator.userAgent, + }); + }) + .catch((e) => { + try { + if (window.parent && window.parent !== window) { + window.parent.postMessage( + { + __bitvid_debug: true, + type: "embed-diag-failed", + payload: { error: String(e) }, + }, + "*" + ); + } + } catch (e2) { + /* no-op */ + } + }) + : Promise.resolve(); const POINTER_PARAM = "pointer"; const PLAYBACK_PARAM = "playback"; @@ -110,9 +103,11 @@ const resolvePlaybackPreference = (value) => { const ensureNostrReady = async () => { try { - await nostrToolsReady; + const toolkitResult = await nostrToolsReady; + diag?.emit("nostr-tools-ready", { ok: Boolean(toolkitResult) }); } catch (error) { userLogger.warn("[embed] Failed to initialize NostrTools:", error); + diag?.emit("nostr-tools-ready", { ok: false, error: String(error) }); } try { @@ -175,13 +170,15 @@ const buildRelayList = (relays) => { ...(Array.isArray(nostrClient.relays) ? nostrClient.relays : []), ]; - return Array.from( + const result = Array.from( new Set( combined .map((relay) => (typeof relay === "string" ? relay.trim() : "")) .filter(Boolean) ) ); + diag?.emit("relay-list", { relays: result }); + return result; }; const resolveEventFromPointer = async (pointer) => { @@ -196,6 +193,7 @@ const resolveEventFromPointer = async (pointer) => { let video = null; if (eventId) { + diag?.emit("event-request", { type: "start", id: eventId }); if (relays.length) { try { const raw = await nostrClient.fetchRawEventById(eventId, { @@ -212,6 +210,12 @@ const resolveEventFromPointer = async (pointer) => { if (!video) { video = await nostrClient.getEventById(eventId); } + diag?.emit("event-request", { + type: "done", + id: eventId, + found: Boolean(video), + videoRootId: video?.videoRootId, + }); } return { @@ -235,6 +239,10 @@ const resolveEventFromPointer = async (pointer) => { return { video: null, pointerHint: null }; } + diag?.emit("event-request", { + type: "start", + naddr: { kind, pubkey, identifier }, + }); await nostrClient.ensurePool(); const pool = nostrClient.pool; const relayList = buildRelayList(relays); @@ -268,6 +276,12 @@ const resolveEventFromPointer = async (pointer) => { } const video = rawEvent ? convertEventToVideo(rawEvent) : null; + diag?.emit("event-request", { + type: "done", + naddr: { kind, pubkey, identifier }, + found: Boolean(video), + videoRootId: video?.videoRootId, + }); return { video, @@ -301,6 +315,10 @@ const setPointerState = ({ video, hint } = {}) => { app.currentVideoPointer = pointerInfo?.pointer || null; app.currentVideoPointerKey = pointerInfo?.key || null; + if (pointerInfo) { + diag?.emit("pointer-resolved", pointerInfo); + } + if (app.currentVideo) { app.currentVideo.pointer = app.currentVideoPointer; app.currentVideo.pointerKey = app.currentVideoPointerKey; @@ -313,6 +331,12 @@ const startEmbed = async () => { } window.__bitvidEmbedStarted = true; + try { + await diagPromise; + } catch (error) { + // Should be caught above, but just in case + } + if (!(videoEl instanceof HTMLVideoElement)) { setStatus("Embed video element is missing.", "error"); return; diff --git a/js/embedDiagnostics.js b/js/embedDiagnostics.js new file mode 100644 index 00000000..acdfbd08 --- /dev/null +++ b/js/embedDiagnostics.js @@ -0,0 +1,177 @@ +// js/embedDiagnostics.js + +let activeEmitter = null; + +export function initEmbedDiagnostics({ enabled = true } = {}) { + if (!enabled) { + return { emit: () => {} }; + } + + function postToParent(type, payload) { + try { + // NOTE: using "*" so tester receives messages; in a production build you may restrict origin. + if (window.parent && window.parent !== window) { + window.parent.postMessage({ __bitvid_debug: true, type, payload }, "*"); + } + } catch (e) { + // ignore + } + } + + function safeStringify(obj, max = 2000) { + try { + const s = JSON.stringify( + obj, + (k, v) => { + if (typeof v === "string" && v.length > 1000) + return v.slice(0, 1000) + "…"; + // redact likely sensitive fields + if (k && /key|secret|sk|priv|pass|token/i.test(k)) return "[REDACTED]"; + return v; + }, + 2 + ); + return s.length > max ? s.slice(0, max) + "…" : s; + } catch (e) { + try { + return String(obj); + } catch (e2) { + return "[unserializable]"; + } + } + } + + // console wrappers + ["log", "info", "warn", "error", "debug"].forEach((lvl) => { + // eslint-disable-next-line no-console + const orig = console[lvl] ? console[lvl].bind(console) : null; + if (orig) { + // eslint-disable-next-line no-console + console[lvl] = (...args) => { + try { + postToParent("console", { + level: lvl, + args: args.map((a) => + typeof a === "string" ? a : safeStringify(a, 1000) + ), + }); + } catch (e) { + /* no-op */ + } + try { + orig(...args); + } catch (e) { + /* no-op */ + } + }; + } + }); + + // global errors + window.addEventListener( + "error", + (ev) => { + try { + const payload = { + message: ev.message, + filename: ev.filename, + lineno: ev.lineno, + colno: ev.colno, + stack: ev.error && ev.error.stack ? ev.error.stack : null, + }; + postToParent("error", payload); + } catch (e) { + /* no-op */ + } + }, + true + ); + + window.addEventListener("unhandledrejection", (ev) => { + try { + const reason = + ev && ev.reason + ? ev.reason && ev.reason.stack + ? ev.reason.stack + : safeStringify(ev.reason) + : "unknown"; + postToParent("unhandledrejection", { reason }); + } catch (e) { + /* no-op */ + } + }); + + // light fetch/XHR monitoring (failures only) + (function wrapFetch() { + if (!window.fetch) return; + const orig = window.fetch.bind(window); + window.fetch = async (...args) => { + try { + const res = await orig(...args); + if (!res.ok) { + postToParent("fetch-error", { + url: args[0], + status: res.status, + statusText: res.statusText, + }); + } + return res; + } catch (err) { + postToParent("fetch-exception", { url: args[0], error: String(err) }); + throw err; + } + }; + })(); + + (function wrapXhr() { + try { + const origOpen = XMLHttpRequest.prototype.open; + const origSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.open = function (...args) { + this.__embed_diag_url = args[1]; + return origOpen.apply(this, args); + }; + XMLHttpRequest.prototype.send = function (...args) { + try { + this.addEventListener("error", () => + postToParent("xhr-error", { url: this.__embed_diag_url }) + ); + this.addEventListener("load", () => { + if (this.status >= 400) + postToParent("xhr-status", { + url: this.__embed_diag_url, + status: this.status, + }); + }); + } catch (e) { + /* no-op */ + } + return origSend.apply(this, args); + }; + } catch (e) { + /* no-op */ + } + })(); + + function emitEmbedEvent(name, payload = {}) { + try { + postToParent("embed-event", { name, payload }); + } catch (e) { + /* no-op */ + } + } + + activeEmitter = emitEmbedEvent; + + return { emit: emitEmbedEvent }; +} + +export function emit(name, payload) { + if (activeEmitter) { + try { + activeEmitter(name, payload); + } catch (e) { + /* no-op */ + } + } +} diff --git a/js/webtorrent.js b/js/webtorrent.js index f3cf7a52..046ad7e1 100644 --- a/js/webtorrent.js +++ b/js/webtorrent.js @@ -22,6 +22,7 @@ import WebTorrent from "./webtorrent.min.js"; import { WSS_TRACKERS } from "./constants.js"; import { safeDecodeURIComponent } from "./utils/safeDecode.js"; import { devLogger, userLogger } from "./utils/logger.js"; +import { emit } from "./embedDiagnostics.js"; const DEFAULT_PROBE_TRACKERS = Object.freeze([...WSS_TRACKERS]); @@ -224,6 +225,8 @@ export class TorrentClient { ? performance.now() : Date.now(); + emit("torrent-probe-start", { magnet: augmentedMagnet }); + return new Promise((resolve) => { let settled = false; let torrent = null; @@ -259,7 +262,7 @@ export class TorrentClient { const isHealthy = Boolean(overrides.healthy); const peersCount = Number.isFinite(overrides.peers) ? overrides.peers : 0; - resolve({ + const result = { healthy: isHealthy, peers: peersCount, reason: isHealthy ? "peer" : "timeout", @@ -268,7 +271,9 @@ export class TorrentClient { usedTrackers: trackers, durationMs: Math.max(0, endedAt - startedAt), ...overrides, - }); + }; + emit("torrent-probe-result", result); + resolve(result); }; try { @@ -859,6 +864,7 @@ export class TorrentClient { */ async streamVideo(magnetURI, videoElement, opts = {}) { try { + emit("torrent-stream-start", { magnet: magnetURI }); // 1) Make sure we have a WebTorrent client and a valid SW registration. const initResult = await this.init(); const serviceWorkerReady =