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.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-24 22:29:18 +00:00
1 parent 868d69cef8
commit 439265d9c0
4 files changed
+270 -45

No files matched your search

+20 -2
View File
@@ -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;
}
+65 -41
View File
@@ -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;
+177
View File
@@ -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 */
}
}
}
+8 -2
View File
@@ -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 =