mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Enable runtime watch history debug logging
This commit is contained in:
1 parent
3792b218a9
commit
614ff3b1f1
4 files changed
+396
-20
No files matched your search
@@ -42,6 +42,7 @@ import WatchHistoryController from "./ui/watchHistoryController.js";
|
||||
import WatchHistoryTelemetry from "./services/watchHistoryTelemetry.js";
|
||||
import { getSidebarLoadingMarkup } from "./sidebarLoading.js";
|
||||
import { subscriptions } from "./subscriptions.js";
|
||||
import { isWatchHistoryDebugEnabled } from "./watchHistoryDebug.js";
|
||||
import {
|
||||
initViewCounter,
|
||||
subscribeToVideoViewCount,
|
||||
@@ -186,7 +187,38 @@ class Application {
|
||||
|
||||
this.nostrService = services.nostrService || nostrService;
|
||||
this.r2Service = services.r2Service || r2Service;
|
||||
this.feedEngine = services.feedEngine || createFeedEngine();
|
||||
|
||||
const feedEngineLogger = (...args) => {
|
||||
if (!isWatchHistoryDebugEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(args) || args.length === 0) {
|
||||
return;
|
||||
}
|
||||
const [prefix, ...rest] = args;
|
||||
if (typeof prefix === "string" && !prefix.includes("watch-history")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (rest.length) {
|
||||
console.info(prefix, ...rest);
|
||||
} else {
|
||||
console.info(prefix);
|
||||
}
|
||||
} catch (error) {
|
||||
if (rest.length) {
|
||||
console.log(prefix, ...rest);
|
||||
} else {
|
||||
console.log(prefix);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.feedEngine =
|
||||
services.feedEngine ||
|
||||
createFeedEngine({
|
||||
logger: feedEngineLogger,
|
||||
});
|
||||
this.payments = services.payments || null;
|
||||
this.splitAndZap =
|
||||
(services.payments && services.payments.splitAndZap) ||
|
||||
|
||||
@@ -4,12 +4,26 @@ import { pointerKey, normalizePointerInput } from "../nostr.js";
|
||||
import watchHistoryService from "../watchHistoryService.js";
|
||||
import nostrService from "../services/nostrService.js";
|
||||
import { createWatchHistoryMetadataResolver } from "../watchHistoryMetadata.js";
|
||||
import {
|
||||
isWatchHistoryDebugEnabled,
|
||||
logWatchHistoryDebug,
|
||||
} from "../watchHistoryDebug.js";
|
||||
import {
|
||||
createBlacklistFilterStage,
|
||||
createWatchHistorySuppressionStage,
|
||||
} from "./stages.js";
|
||||
import { isPlainObject } from "./utils.js";
|
||||
|
||||
const WATCH_HISTORY_FEED_LOG_NAMESPACE = "watchHistoryFeed";
|
||||
|
||||
function debugInfo(message, details) {
|
||||
logWatchHistoryDebug(WATCH_HISTORY_FEED_LOG_NAMESPACE, "info", message, details);
|
||||
}
|
||||
|
||||
function debugWarn(message, details) {
|
||||
logWatchHistoryDebug(WATCH_HISTORY_FEED_LOG_NAMESPACE, "warn", message, details);
|
||||
}
|
||||
|
||||
function normalizeActorCandidate(...values) {
|
||||
for (const value of values) {
|
||||
if (typeof value === "string") {
|
||||
@@ -213,6 +227,11 @@ function createWatchHistoryHydratorStage({
|
||||
? item.pointerKey
|
||||
: pointerKey(item.pointer);
|
||||
|
||||
const pointerType =
|
||||
typeof item?.pointer?.type === "string" ? item.pointer.type : null;
|
||||
const pointerValue =
|
||||
typeof item?.pointer?.value === "string" ? item.pointer.value : null;
|
||||
|
||||
const watchedAtValue = Number.isFinite(item?.watchedAt)
|
||||
? Math.max(0, Math.floor(Number(item.watchedAt)))
|
||||
: Number.isFinite(item?.metadata?.watchedAt)
|
||||
@@ -222,40 +241,133 @@ function createWatchHistoryHydratorStage({
|
||||
let video = item.video || null;
|
||||
let profile = item.metadata?.profile || null;
|
||||
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Hydrating watch history item.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pointerType,
|
||||
pointerValue,
|
||||
hasInitialVideo: Boolean(video),
|
||||
hasInitialProfile: Boolean(profile),
|
||||
watchedAt: watchedAtValue || null,
|
||||
});
|
||||
}
|
||||
|
||||
if (pointerKeyValue && typeof service?.getLocalMetadata === "function") {
|
||||
try {
|
||||
const stored = service.getLocalMetadata(pointerKeyValue);
|
||||
if (stored) {
|
||||
const hadVideo = Boolean(video);
|
||||
const hadProfile = Boolean(profile);
|
||||
video = video || stored.video || null;
|
||||
profile = profile || stored.profile || null;
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Loaded cached metadata for pointer.", {
|
||||
pointerKey: pointerKeyValue,
|
||||
hadVideo,
|
||||
hadProfile,
|
||||
cachedVideo: Boolean(stored.video),
|
||||
cachedProfile: Boolean(stored.profile),
|
||||
resultingVideo: Boolean(video),
|
||||
resultingProfile: Boolean(profile),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[watch-history-feed] Failed to read cached metadata",
|
||||
error,
|
||||
);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugWarn("Failed to read cached metadata for pointer.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!video && resolver?.resolveVideo) {
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Resolving video via metadata resolver.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pointerType,
|
||||
pointerValue,
|
||||
});
|
||||
}
|
||||
try {
|
||||
video = await resolver.resolveVideo(item.pointer);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
if (video) {
|
||||
debugInfo("Metadata resolver returned video candidate.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
videoId: typeof video.id === "string" ? video.id : null,
|
||||
title:
|
||||
typeof video.title === "string" && video.title.trim()
|
||||
? video.title
|
||||
: null,
|
||||
invalid: Boolean(video.invalid),
|
||||
});
|
||||
} else {
|
||||
debugWarn("Metadata resolver returned no video candidate.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[watch-history-feed] Failed to resolve video from pointer",
|
||||
error,
|
||||
);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugWarn("Metadata resolver threw while resolving video.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (video?.pubkey && !profile && resolver?.resolveProfile) {
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Resolving profile for video author.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pubkey: video.pubkey,
|
||||
});
|
||||
}
|
||||
try {
|
||||
profile = resolver.resolveProfile(video.pubkey) || null;
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
if (profile) {
|
||||
const label =
|
||||
profile.display_name ||
|
||||
profile.name ||
|
||||
profile.username ||
|
||||
profile.nip05 ||
|
||||
null;
|
||||
debugInfo("Resolved profile for video author.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pubkey: video.pubkey,
|
||||
label: label || null,
|
||||
});
|
||||
} else {
|
||||
debugWarn("Resolver returned no profile for video author.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pubkey: video.pubkey,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[watch-history-feed] Failed to resolve profile for pointer",
|
||||
error,
|
||||
);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugWarn("Profile resolver threw while hydrating metadata.", {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pubkey: video.pubkey,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +382,27 @@ function createWatchHistoryHydratorStage({
|
||||
profile: profile || null,
|
||||
};
|
||||
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
const summary = {
|
||||
pointerKey: pointerKeyValue || null,
|
||||
pointerType,
|
||||
pointerValue,
|
||||
watchedAt: watchedAtValue || null,
|
||||
videoId: typeof video?.id === "string" ? video.id : null,
|
||||
title:
|
||||
typeof video?.title === "string" && video.title?.trim()
|
||||
? video.title
|
||||
: null,
|
||||
invalid: Boolean(video?.invalid),
|
||||
hasProfile: Boolean(profile),
|
||||
};
|
||||
if (video) {
|
||||
debugInfo("Hydrated watch history entry metadata.", summary);
|
||||
} else {
|
||||
debugWarn("Hydrated entry is missing resolved video metadata.", summary);
|
||||
}
|
||||
}
|
||||
|
||||
const nextItem = {
|
||||
...item,
|
||||
pointerKey: pointerKeyValue,
|
||||
@@ -286,20 +419,44 @@ function createWatchHistoryHydratorStage({
|
||||
video: metadata.video,
|
||||
profile: metadata.profile,
|
||||
});
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Stored metadata cache entry for pointer.", {
|
||||
pointerKey: pointerKeyValue,
|
||||
cachedVideo: Boolean(metadata.video),
|
||||
cachedProfile: Boolean(metadata.profile),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[watch-history-feed] Failed to persist metadata cache",
|
||||
error,
|
||||
);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugWarn("Failed to persist metadata cache entry.", {
|
||||
pointerKey: pointerKeyValue,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (typeof service?.removeLocalMetadata === "function") {
|
||||
try {
|
||||
service.removeLocalMetadata(pointerKeyValue);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugInfo("Cleared metadata cache entry for pointer.", {
|
||||
pointerKey: pointerKeyValue,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
context?.log?.(
|
||||
"[watch-history-feed] Failed to clear cached metadata",
|
||||
error,
|
||||
);
|
||||
if (isWatchHistoryDebugEnabled()) {
|
||||
debugWarn("Failed to clear cached metadata entry.", {
|
||||
pointerKey: pointerKeyValue,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
// js/watchHistoryDebug.js
|
||||
|
||||
const WATCH_HISTORY_DEBUG_STORAGE_KEY =
|
||||
"bitvid:debug:watch-history";
|
||||
|
||||
const globalScope =
|
||||
typeof globalThis !== "undefined"
|
||||
? globalThis
|
||||
: typeof window !== "undefined"
|
||||
? window
|
||||
: typeof self !== "undefined"
|
||||
? self
|
||||
: null;
|
||||
|
||||
const TRUTHY_VALUES = new Set([
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
"enable",
|
||||
"enabled",
|
||||
"debug",
|
||||
"verbose",
|
||||
"watch",
|
||||
]);
|
||||
|
||||
const FALSY_VALUES = new Set([
|
||||
"0",
|
||||
"false",
|
||||
"no",
|
||||
"off",
|
||||
"disable",
|
||||
"disabled",
|
||||
"quiet",
|
||||
"silent",
|
||||
]);
|
||||
|
||||
function parseFlagValue(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return true;
|
||||
}
|
||||
if (TRUTHY_VALUES.has(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (FALSY_VALUES.has(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function safeReadStorageFlag(storage) {
|
||||
if (!storage || typeof storage.getItem !== "function") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const raw = storage.getItem(WATCH_HISTORY_DEBUG_STORAGE_KEY);
|
||||
if (raw === null || raw === undefined) {
|
||||
return null;
|
||||
}
|
||||
return parseFlagValue(String(raw));
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeWriteStorageFlag(storage, value) {
|
||||
if (!storage || typeof storage.setItem !== "function") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
storage.setItem(WATCH_HISTORY_DEBUG_STORAGE_KEY, value ? "true" : "false");
|
||||
} catch (error) {
|
||||
// Ignore storage errors (e.g., quota exceeded, private mode restrictions).
|
||||
}
|
||||
}
|
||||
|
||||
function persistFlag(scope, value) {
|
||||
if (value === null) {
|
||||
return;
|
||||
}
|
||||
safeWriteStorageFlag(scope?.sessionStorage, value);
|
||||
safeWriteStorageFlag(scope?.localStorage, value);
|
||||
}
|
||||
|
||||
function readFlagFromUrl(scope) {
|
||||
if (!scope || typeof scope.location?.search !== "string") {
|
||||
return null;
|
||||
}
|
||||
const search = scope.location.search;
|
||||
if (!search) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const params = new URLSearchParams(search);
|
||||
if (!params.has("watchHistoryDebug")) {
|
||||
return null;
|
||||
}
|
||||
const parsed = parseFlagValue(params.get("watchHistoryDebug"));
|
||||
if (parsed === null) {
|
||||
return null;
|
||||
}
|
||||
persistFlag(scope, parsed);
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveExplicitFlag(scope) {
|
||||
if (!scope) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const runtimeFlag = scope.__BITVID_ENABLE_WATCH_HISTORY_DEBUG__;
|
||||
if (runtimeFlag === true || runtimeFlag === false) {
|
||||
return Boolean(runtimeFlag);
|
||||
}
|
||||
|
||||
const altRuntimeFlag = scope.BITVID_WATCH_HISTORY_DEBUG;
|
||||
if (altRuntimeFlag === true || altRuntimeFlag === false) {
|
||||
return Boolean(altRuntimeFlag);
|
||||
}
|
||||
|
||||
const urlFlag = readFlagFromUrl(scope);
|
||||
if (urlFlag !== null) {
|
||||
return urlFlag;
|
||||
}
|
||||
|
||||
const sessionFlag = safeReadStorageFlag(scope.sessionStorage);
|
||||
if (sessionFlag !== null) {
|
||||
return sessionFlag;
|
||||
}
|
||||
|
||||
const localFlag = safeReadStorageFlag(scope.localStorage);
|
||||
if (localFlag !== null) {
|
||||
return localFlag;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isWatchHistoryDebugEnabled() {
|
||||
const scope = globalScope;
|
||||
const explicitFlag = resolveExplicitFlag(scope);
|
||||
if (explicitFlag !== null) {
|
||||
return explicitFlag;
|
||||
}
|
||||
|
||||
if (typeof process !== "undefined" && process?.env?.NODE_ENV) {
|
||||
return process.env.NODE_ENV !== "production";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function logWatchHistoryDebug(namespace, level, message, details) {
|
||||
if (!isWatchHistoryDebugEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scopeConsole =
|
||||
(globalScope && globalScope.console) || (typeof console !== "undefined" ? console : null);
|
||||
if (!scopeConsole) {
|
||||
return;
|
||||
}
|
||||
|
||||
const methodName =
|
||||
typeof level === "string" && level && typeof scopeConsole[level] === "function"
|
||||
? level
|
||||
: typeof scopeConsole.info === "function"
|
||||
? "info"
|
||||
: "log";
|
||||
|
||||
const prefix = namespace ? `[${namespace}] ${message}` : message;
|
||||
|
||||
try {
|
||||
if (details && typeof details === "object") {
|
||||
scopeConsole[methodName](prefix, details);
|
||||
} else {
|
||||
scopeConsole[methodName](prefix);
|
||||
}
|
||||
} catch (error) {
|
||||
try {
|
||||
if (details && typeof details === "object") {
|
||||
scopeConsole.log(prefix, details);
|
||||
} else {
|
||||
scopeConsole.log(prefix);
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
// Swallow logging errors entirely.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,30 +3,14 @@
|
||||
import { getApplication } from "./applicationContext.js";
|
||||
import nostrService from "./services/nostrService.js";
|
||||
import { nostrClient } from "./nostr.js";
|
||||
|
||||
const isDevEnv =
|
||||
typeof process !== "undefined" && process?.env?.NODE_ENV !== "production";
|
||||
import { logWatchHistoryDebug } from "./watchHistoryDebug.js";
|
||||
|
||||
function logInfo(message, details) {
|
||||
if (!isDevEnv) {
|
||||
return;
|
||||
}
|
||||
if (details && typeof details === "object") {
|
||||
console.info(`[watchHistoryMetadata] ${message}`, details);
|
||||
} else {
|
||||
console.info(`[watchHistoryMetadata] ${message}`);
|
||||
}
|
||||
logWatchHistoryDebug("watchHistoryMetadata", "info", message, details);
|
||||
}
|
||||
|
||||
function logWarn(message, details) {
|
||||
if (!isDevEnv) {
|
||||
return;
|
||||
}
|
||||
if (details && typeof details === "object") {
|
||||
console.warn(`[watchHistoryMetadata] ${message}`, details);
|
||||
} else {
|
||||
console.warn(`[watchHistoryMetadata] ${message}`);
|
||||
}
|
||||
logWatchHistoryDebug("watchHistoryMetadata", "warn", message, details);
|
||||
}
|
||||
|
||||
function normalizeVideoCandidate(candidate) {
|
||||
|
||||
Reference in new issue
Block a user