add more robust logging

This commit is contained in:
kmmiio99o 2026-01-09 21:28:58 +01:00
parent 7cb2d1aa9c
commit 0cca83ff07
No known key found for this signature in database
GPG key ID: 163FE39F7772D2EC
2 changed files with 237 additions and 37 deletions

View file

@ -10,6 +10,108 @@ import { serviceFactory } from "./services/ServiceFactory";
import Settings from "./ui/pages/Settings"; import Settings from "./ui/pages/Settings";
import patchSidebar from "./sidebar"; import patchSidebar from "./sidebar";
(function installUnhandledRejectionHandler() {
try {
const g = typeof globalThis !== "undefined" ? (globalThis as any) : {};
// Avoid installing multiple times
if (g && !g.__scrobble_unhandled_rejection_installed) {
const handler = (ev: any) => {
try {
// ev may be an event or a reason object depending on the runtime
const reason = ev && (ev.reason ?? ev) ? (ev.reason ?? ev) : ev;
console.error("[Scrobble Plugin] Unhandled rejection:", reason);
} catch (inner) {
// Defensive: if formatting the event fails, still log something
console.error("[Scrobble Plugin] Unhandled rejection (unknown format)", ev);
}
};
// Prefer globalThis.addEventListener
if (g && typeof g.addEventListener === "function") {
try {
g.addEventListener("unhandledrejection", handler);
g.__scrobble_unhandled_rejection_installed = true;
return;
} catch (e) {
// continue to other fallbacks
console.error("[Scrobble Plugin] Failed to register via globalThis.addEventListener:", e);
}
}
// If window exists and supports addEventListener, use it
if (typeof (globalThis as any).window !== "undefined") {
const w = (globalThis as any).window;
if (w && typeof w.addEventListener === "function") {
try {
w.addEventListener("unhandledrejection", handler);
g.__scrobble_unhandled_rejection_installed = true;
return;
} catch (e) {
console.error("[Scrobble Plugin] Failed to register via window.addEventListener:", e);
}
} else if (w) {
// fallback to assigning onunhandledrejection if available and not a function call
try {
if (typeof w.onunhandledrejection === "undefined") {
w.onunhandledrejection = (ev: any) => handler(ev);
g.__scrobble_unhandled_rejection_installed = true;
return;
}
} catch (e) {
console.error("[Scrobble Plugin] Failed to assign window.onunhandledrejection:", e);
}
}
}
// As a last resort, try process.on (Node-style). Guard existence and function type.
try {
const proc = (globalThis as any).process;
if (proc && typeof proc.on === "function") {
proc.on("unhandledRejection", (reason: any) => handler({ reason }));
g.__scrobble_unhandled_rejection_installed = true;
return;
}
} catch (e) {
// ignore
}
// If none of the above worked, set a marker so we don't keep trying on repeated loads
g.__scrobble_unhandled_rejection_installed = true;
}
} catch (e) {
// Most defensive: don't let any error here block plugin loading
try {
console.error("[Scrobble Plugin] Error while installing unhandled rejection handler:", e);
} catch {
/* swallow */
}
}
})();
// Heartbeat helpers
let __scrobbleHeartbeat: any;
function startHeartbeat() {
if (__scrobbleHeartbeat) return;
try {
__scrobbleHeartbeat = setInterval(() => {
console.log("[Scrobble Plugin] Heartbeat (alive)");
}, 60_000);
} catch (e) {
console.error("[Scrobble Plugin] Failed to start heartbeat:", e);
}
}
function stopHeartbeat() {
if (!__scrobbleHeartbeat) return;
try {
clearInterval(__scrobbleHeartbeat);
} catch (e) {
console.error("[Scrobble Plugin] Failed to clear heartbeat:", e);
} finally {
__scrobbleHeartbeat = undefined;
}
}
export const pluginState = { export const pluginState = {
pluginStopped: false, pluginStopped: false,
lastActivity: undefined, lastActivity: undefined,
@ -28,18 +130,29 @@ let sidebarUnpatch: (() => void) | undefined;
// Set up default plugin settings // Set up default plugin settings
const defaultSettings: LFMSettings = Constants.DEFAULT_SETTINGS; const defaultSettings: LFMSettings = Constants.DEFAULT_SETTINGS;
for (const key of Object.keys(defaultSettings)) { for (const key of Object.keys(defaultSettings)) {
plugin.storage[key] = // Defensive: plugin.storage may not accept arbitrary keys in some environments,
plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings]; // but assignment should generally be fine in Vendetta.
try {
plugin.storage[key] =
plugin.storage[key] ?? defaultSettings[key as keyof typeof defaultSettings];
} catch (e) {
console.error("[Scrobble Plugin] Failed to initialize default setting", key, e);
}
} }
plugin.storage.addToSidebar ??= false; try {
// safe default for addToSidebar
plugin.storage.addToSidebar ??= false;
} catch (e) {
// ignore if storage access fails
}
export const currentSettings = new Proxy(plugin.storage, { export const currentSettings = new Proxy(plugin.storage, {
get(target, prop) { get(target, prop) {
return target[prop]; return (target as any)[prop];
}, },
set(target, prop, value) { set(target, prop, value) {
target[prop] = value; (target as any)[prop] = value;
return true; return true;
}, },
}); });
@ -50,8 +163,7 @@ const MAX_CONNECTION_ATTEMPTS = 3;
const RECONNECT_DELAY = 5000; const RECONNECT_DELAY = 5000;
const log = (...args: any[]) => console.log("[Scrobble Plugin]", ...args); const log = (...args: any[]) => console.log("[Scrobble Plugin]", ...args);
const logError = (...args: any[]) => const logError = (...args: any[]) => console.error("[Scrobble Plugin] Error:", ...args);
console.error("[Scrobble Plugin] Error:", ...args);
async function tryInitialize() { async function tryInitialize() {
try { try {
@ -63,9 +175,7 @@ async function tryInitialize() {
connectionAttempts++; connectionAttempts++;
if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) { if (connectionAttempts < MAX_CONNECTION_ATTEMPTS) {
log( log(`Retrying connection (${connectionAttempts}/${MAX_CONNECTION_ATTEMPTS})...`);
`Retrying connection (${connectionAttempts}/${MAX_CONNECTION_ATTEMPTS})...`,
);
setTimeout(tryInitialize, RECONNECT_DELAY); setTimeout(tryInitialize, RECONNECT_DELAY);
} else { } else {
logError(`Failed to connect after ${MAX_CONNECTION_ATTEMPTS} attempts`); logError(`Failed to connect after ${MAX_CONNECTION_ATTEMPTS} attempts`);
@ -80,7 +190,12 @@ async function validateAndInitialize() {
return; return;
} }
const serviceName = serviceFactory.getCurrentService().getServiceName(); let serviceName = "Unknown";
try {
serviceName = serviceFactory.getCurrentService().getServiceName();
} catch (e) {
logError("Failed to determine current service name:", e);
}
// Make sure we have credentials for whatever service is selected // Make sure we have credentials for whatever service is selected
const service = currentSettings.service; const service = currentSettings.service;
@ -91,9 +206,7 @@ async function validateAndInitialize() {
hasCredentials = !!(currentSettings.username && currentSettings.apiKey); hasCredentials = !!(currentSettings.username && currentSettings.apiKey);
break; break;
case "librefm": case "librefm":
hasCredentials = !!( hasCredentials = !!(currentSettings.librefmUsername && currentSettings.librefmApiKey);
currentSettings.librefmUsername && currentSettings.librefmApiKey
);
break; break;
case "listenbrainz": case "listenbrainz":
hasCredentials = !!currentSettings.listenbrainzUsername; hasCredentials = !!currentSettings.listenbrainzUsername;
@ -101,9 +214,7 @@ async function validateAndInitialize() {
} }
if (!hasCredentials) { if (!hasCredentials) {
logError( logError(`Missing credentials for ${serviceName}. Please configure in plugin settings.`);
`Missing credentials for ${serviceName}. Please configure in plugin settings.`,
);
return; return;
} }
@ -115,14 +226,22 @@ async function validateAndInitialize() {
} else { } else {
log("Waiting for Discord connection..."); log("Waiting for Discord connection...");
const waitForUser = () => { const waitForUser = () => {
if (UserStore.getCurrentUser()) { try {
log("Discord connection established"); if (UserStore.getCurrentUser()) {
tryInitialize(); log("Discord connection established");
FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser); tryInitialize();
FluxDispatcher.unsubscribe("CONNECTION_OPEN", waitForUser);
}
} catch (e) {
logError("Error while waiting for Discord connection:", e);
} }
}; };
FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser); try {
FluxDispatcher.subscribe("CONNECTION_OPEN", waitForUser);
} catch (e) {
logError("Failed to subscribe to CONNECTION_OPEN:", e);
}
} }
} }
@ -131,15 +250,27 @@ export default {
log("Plugin loading..."); log("Plugin loading...");
pluginState.pluginStopped = false; pluginState.pluginStopped = false;
// Start heartbeat so we can confirm plugin process is alive
try {
startHeartbeat();
log("Heartbeat started");
} catch (e) {
logError("Failed to start heartbeat:", e);
}
// Check if service is configured // Check if service is configured
if (!currentSettings.service) { if (!currentSettings.service) {
log("No service configured. Please select a service in plugin settings."); log("No service configured. Please select a service in plugin settings.");
} else { } else {
// Show what we're starting with // Show what we're starting with
const serviceName = serviceFactory.getCurrentService().getServiceName(); try {
log( const serviceName = serviceFactory.getCurrentService().getServiceName();
`Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`, log(
); `Configuration: Service=${serviceName}, Update Interval=${currentSettings.timeInterval}s, Verbose=${currentSettings.verboseLogging}`,
);
} catch (e) {
logError("Failed to read service configuration:", e);
}
} }
// Add to sidebar if user wants it // Add to sidebar if user wants it
@ -147,7 +278,11 @@ export default {
sidebarUnpatch = patchSidebar(); sidebarUnpatch = patchSidebar();
cleanupFunctions.push(() => { cleanupFunctions.push(() => {
if (sidebarUnpatch) { if (sidebarUnpatch) {
sidebarUnpatch(); try {
sidebarUnpatch();
} catch (e) {
// ignore individual cleanup errors
}
sidebarUnpatch = undefined; sidebarUnpatch = undefined;
} }
}); });
@ -155,13 +290,26 @@ export default {
log("Sidebar setup failed:", error); log("Sidebar setup failed:", error);
} }
validateAndInitialize(); try {
validateAndInitialize();
log("onLoad complete - validateAndInitialize triggered");
} catch (e) {
logError("validateAndInitialize threw an error:", e);
}
}, },
onUnload() { onUnload() {
log("Plugin unloading..."); log("Plugin unloading...");
pluginState.pluginStopped = true; pluginState.pluginStopped = true;
// Stop heartbeat first to avoid stray logs after unload
try {
stopHeartbeat();
log("Heartbeat stopped");
} catch (e) {
logError("Failed to stop heartbeat:", e);
}
// Run all cleanup functions // Run all cleanup functions
cleanupFunctions.forEach((cleanup) => { cleanupFunctions.forEach((cleanup) => {
try { try {
@ -172,7 +320,11 @@ export default {
}); });
cleanupFunctions.length = 0; cleanupFunctions.length = 0;
stop(); try {
stop();
} catch (e) {
logError("Error while stopping plugin manager:", e);
}
log("Plugin unloaded"); log("Plugin unloaded");
}, },
@ -184,9 +336,13 @@ export default {
const newSidebar = newSettings.addToSidebar; const newSidebar = newSettings.addToSidebar;
// Apply the new settings // Apply the new settings
Object.assign(currentSettings, newSettings); try {
Object.assign(currentSettings, newSettings);
} catch (e) {
logError("Failed to apply new settings:", e);
}
log("Settings updated:", Object.keys(newSettings)); log("Settings updated:", Object.keys(newSettings || {}));
// Check if sidebar setting changed // Check if sidebar setting changed
if (oldSidebar !== newSidebar) { if (oldSidebar !== newSidebar) {
@ -199,7 +355,11 @@ export default {
} }
} else { } else {
if (sidebarUnpatch) { if (sidebarUnpatch) {
sidebarUnpatch(); try {
sidebarUnpatch();
} catch (e) {
logError("Failed to unpatch sidebar:", e);
}
sidebarUnpatch = undefined; sidebarUnpatch = undefined;
log("Sidebar disabled"); log("Sidebar disabled");
} }
@ -209,7 +369,11 @@ export default {
// Switch services if needed // Switch services if needed
if (oldService !== newService && newService) { if (oldService !== newService && newService) {
log(`Service changed from ${oldService || "none"} to ${newService}`); log(`Service changed from ${oldService || "none"} to ${newService}`);
await switchService(newService); try {
await switchService(newService);
} catch (e) {
logError("Failed to switch service:", e);
}
} else if (!pluginState.pluginStopped && currentSettings.service) { } else if (!pluginState.pluginStopped && currentSettings.service) {
// Just restart with the updated settings (only if service is selected) // Just restart with the updated settings (only if service is selected)
log("Restarting with updated settings..."); log("Restarting with updated settings...");
@ -217,7 +381,11 @@ export default {
} else if (!currentSettings.service) { } else if (!currentSettings.service) {
// Service was unselected, stop the plugin // Service was unselected, stop the plugin
log("Service unselected, stopping plugin..."); log("Service unselected, stopping plugin...");
stop(); try {
stop();
} catch (e) {
logError("Error while stopping due to service unselected:", e);
}
} }
}, },

View file

@ -53,8 +53,14 @@ class PluginManager {
// Check for new tracks and update Discord status when something changes // Check for new tracks and update Discord status when something changes
private async updateActivity() { private async updateActivity() {
if (pluginState.pluginStopped) { if (pluginState.pluginStopped) {
log("Plugin is stopped; skipping activity updates and clearing timers");
logVerbose("Plugin is stopped, skipping update"); logVerbose("Plugin is stopped, skipping update");
this.stopUpdates(); try {
this.stopUpdates();
logVerbose("Update timers cleared due to stopped plugin");
} catch (e) {
logError("Error while stopping updates for stopped plugin:", e);
}
return; return;
} }
@ -75,10 +81,16 @@ class PluginManager {
}); });
if (ignoredActivity) { if (ignoredActivity) {
log(`Ignored app (${ignoredActivity.name}) is currently active; clearing activity and skipping updates`);
logVerbose( logVerbose(
`Ignored app (${ignoredActivity.name}) is currently playing, clearing activity`, `Ignored app (${ignoredActivity.name}) is currently playing, clearing activity`,
); );
// Remove these setDebugInfo calls that cause TypeScript errors try {
// Provide a little debug context so we can see why updates are skipped
setDebugInfo("ignoredActivity", ignoredActivity.name);
} catch (e) {
logVerbose("Failed to set debug info for ignored activity:", e);
}
clearActivity(); clearActivity();
return; return;
} }
@ -91,13 +103,20 @@ class PluginManager {
setDebugInfo("lastTrack", lastTrack); setDebugInfo("lastTrack", lastTrack);
if (!lastTrack.nowPlaying) { if (!lastTrack.nowPlaying) {
log("No currently playing track reported by service; clearing activity and skipping RPC update");
logVerbose("Track is not currently playing"); logVerbose("Track is not currently playing");
try {
setDebugInfo("lastTrack_nowPlaying", false);
} catch (e) {
logVerbose("Failed to set debug info for nowPlaying:", e);
}
clearActivity(); clearActivity();
return; return;
} }
// use simple URL comparison like Last.fm plugin // use simple URL comparison like Last.fm plugin
if (pluginState.lastTrackUrl === lastTrack.url) { if (pluginState.lastTrackUrl === lastTrack.url) {
log("Track unchanged; skipping Discord RPC update");
logVerbose("Track hasn't changed"); logVerbose("Track hasn't changed");
recordSuccessfulUpdate(); recordSuccessfulUpdate();
this.consecutiveFailures = 0; this.consecutiveFailures = 0;
@ -255,7 +274,20 @@ class PluginManager {
); );
} catch (error) { } catch (error) {
logError("Update failed:", error); logError("Update failed:", error);
recordServiceError(currentSettings.service, (error as Error).message); try {
recordServiceError(currentSettings.service, (error as Error).message);
} catch (e) {
logError("Failed to record service error:", e);
}
try {
setDebugInfo("lastUpdateError", {
message: (error as Error).message,
service: currentSettings.service,
lastTrackUrl: pluginState.lastTrackUrl,
});
} catch (e) {
logVerbose("Failed to set debug info for last update error:", e);
}
this.handleError(error as Error); this.handleError(error as Error);
} }
} }