From 9d3ca46533e7d4e4c1afef3480c4604f5bd752e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:01:34 +0000 Subject: [PATCH 1/3] Initial plan From 466f9b92f4c15e294b8d1cb20ee1a418e160c70c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:18:37 +0000 Subject: [PATCH 2/3] Add structured logging infrastructure: Logger module + replace all console.* calls Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com> --- package-lock.json | 10 +-- src/background/service-worker.ts | 116 ++++++++++++++++-------------- src/config/environment.ts | 14 ++-- src/offscreen/offscreen.ts | 10 ++- src/options/options.tsx | 7 +- src/popup/AuthForm.tsx | 7 +- src/popup/popup.tsx | 25 ++++--- src/services/AuthService.ts | 5 +- src/services/NumbersApiManager.ts | 11 +-- src/services/ScreenshotService.ts | 7 +- src/services/UploadService.ts | 41 +++++------ src/share/share.tsx | 5 +- src/utils/logger.ts | 93 ++++++++++++++++++++++++ 13 files changed, 238 insertions(+), 113 deletions(-) create mode 100644 src/utils/logger.ts diff --git a/package-lock.json b/package-lock.json index 9c91664..26a975e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "proofsnap-extension", - "version": "1.1.1", + "version": "1.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "proofsnap-extension", - "version": "1.1.1", + "version": "1.1.4", "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" @@ -49,7 +49,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1156,7 +1155,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1267,7 +1265,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -1709,7 +1706,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "peer": true, "engines": { "node": ">=12" }, @@ -1769,7 +1765,6 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2000,7 +1995,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/src/background/service-worker.ts b/src/background/service-worker.ts index 479f27b..ee4d254 100644 --- a/src/background/service-worker.ts +++ b/src/background/service-worker.ts @@ -7,8 +7,16 @@ import { storageService } from '../services/StorageService'; import { indexedDBService } from '../services/IndexedDBService'; import { getNumbersApi } from '../services/NumbersApiManager'; import { ExtensionMessage, CaptureScreenshotMessage } from '../types'; +import { createLogger } from '../utils/logger'; -console.log('ProofSnap background service worker loaded'); +const logger = createLogger('ServiceWorker'); + +// Catch unhandled promise rejections to surface silent failures +self.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => { + logger.error('Unhandled promise rejection', event.reason); +}); + +logger.info('ProofSnap background service worker loaded'); // Use singleton service instances const assetStorage = indexedDBService; @@ -19,28 +27,28 @@ Promise.all([ assetStorage.init(), metadataStorage.init() ]).then(async () => { - console.log('Storage services initialized (IndexedDB + chrome.storage ready)'); + logger.info('Storage services initialized (IndexedDB + chrome.storage ready)'); // Initialize NumbersApiManager and register upload completion callback try { const numbersApi = await getNumbersApi(); numbersApi.upload.onUploadComplete((assetId: string) => { - console.log('📥 Upload completion callback triggered for asset:', assetId); + logger.debug('Upload completion callback triggered for asset', { assetId }); updateExtensionBadge(); }); - console.log('Upload completion callback registered'); + logger.info('Upload completion callback registered'); } catch (error) { - console.error('Failed to initialize NumbersApiManager:', error); + logger.error('Failed to initialize NumbersApiManager', error); } }).catch(error => { - console.error('Failed to initialize services:', error); + logger.error('Failed to initialize services', error); }); /** * Handle extension installation */ chrome.runtime.onInstalled.addListener((details) => { - console.log('Extension installed:', details.reason); + logger.info('Extension installed', { reason: details.reason }); if (details.reason === 'install') { // Set default settings on first install @@ -73,7 +81,7 @@ chrome.runtime.onInstalled.addListener((details) => { * Handle keyboard shortcut commands */ chrome.commands.onCommand.addListener(async (command) => { - console.log('Command received:', command); + logger.debug('Command received', { command }); if (command === 'capture-screenshot') { await handleScreenshotCapture('visible'); @@ -84,7 +92,7 @@ chrome.commands.onCommand.addListener(async (command) => { * Handle extension icon click */ chrome.action.onClicked.addListener(async (tab) => { - console.log('Extension icon clicked', tab); + logger.debug('Extension icon clicked', { tabId: tab.id }); // The popup will open automatically, no need to handle here }); @@ -92,7 +100,7 @@ chrome.action.onClicked.addListener(async (tab) => { * Handle messages from popup */ chrome.runtime.onMessage.addListener((message: ExtensionMessage, _sender, sendResponse) => { - console.log('Message received:', message.type, message.payload); + logger.debug('Message received', { type: message.type }); switch (message.type) { case 'CAPTURE_SCREENSHOT': @@ -114,23 +122,23 @@ chrome.runtime.onMessage.addListener((message: ExtensionMessage, _sender, sendRe return true; case 'START_GOOGLE_AUTH': - console.log('Starting Google Auth in background...'); + logger.info('Starting Google Auth in background'); (async () => { try { const numbersApi = await getNumbersApi(); // 1. Get ID Token via Chrome Identity (interactive flow) - console.log('Background: Requesting Google ID Token...'); + logger.debug('Requesting Google ID Token'); const token = await numbersApi.auth.authenticateWithGoogle(); - console.log('Background: Got ID Token. Logging in to backend...'); + logger.debug('Got ID Token. Logging in to backend'); // 2. Exchange ID Token for numbers protocol auth token await numbersApi.loginGoogle(token); - console.log('Background: Google Login successful.'); + logger.info('Google Login successful'); sendResponse({ success: true }); } catch (error: any) { - console.error('Background: Google Auth failed:', error); + logger.error('Google Auth failed', error); const errorMessage = error.message || 'Google Auth failed'; await storageService.setGoogleAuthError(errorMessage); sendResponse({ success: false, error: errorMessage }); @@ -145,7 +153,7 @@ chrome.runtime.onMessage.addListener((message: ExtensionMessage, _sender, sendRe return false; default: - console.warn('Unknown message type:', message.type); + logger.warn('Unknown message type', { type: message.type }); sendResponse({ success: false, error: 'Unknown message type' }); } }); @@ -179,7 +187,7 @@ async function handleSelectionCapture(tab: chrome.tabs.Tab): Promise { files: ['content/selection-overlay.js'], }); } catch (error) { - console.error('Failed to inject selection script:', error); + logger.error('Failed to inject selection script', error); throw new Error('Failed to start selection mode. Make sure you are on a valid web page.'); } @@ -204,7 +212,7 @@ async function handleSelectionCapture(tab: chrome.tabs.Tab): Promise { */ async function handleSelectionComplete(payload: any) { if (payload.cancelled) { - console.log('Selection cancelled:', payload.reason); + logger.debug('Selection cancelled', { reason: payload.reason }); if (pendingSelectionResolve) { pendingSelectionResolve({ cancelled: true, reason: payload.reason }); pendingSelectionResolve = null; @@ -214,7 +222,7 @@ async function handleSelectionComplete(payload: any) { } const { coordinates } = payload; - console.log('Selection complete:', coordinates); + logger.debug('Selection complete', { coordinates }); try { // Get the active tab to capture @@ -256,9 +264,9 @@ async function handleSelectionComplete(payload: any) { if (response.success) { dataUrl = response.data.dataUrl; - console.log('✅ Selection cropped and watermark added'); + logger.info('Selection cropped and watermark added'); } else { - console.warn('Failed to process selection:', response.error); + logger.warn('Failed to process selection', { error: response.error }); } // Get location if enabled via offscreen document @@ -271,12 +279,12 @@ async function handleSelectionComplete(payload: any) { }); if (locationResponse.success && locationResponse.data) { gpsLocation = locationResponse.data; - console.log('✅ Geolocation captured:', gpsLocation!.latitude, gpsLocation!.longitude); + logger.info('Geolocation captured', { latitude: gpsLocation!.latitude, longitude: gpsLocation!.longitude }); } else { - console.warn('⚠️ Could not get geolocation:', locationResponse.error || 'Permission denied or unavailable'); + logger.warn('Could not get geolocation', { error: locationResponse.error || 'Permission denied or unavailable' }); } } catch (error) { - console.warn('⚠️ Geolocation error:', error); + logger.warn('Geolocation error', { error }); // Continue without location } } @@ -290,7 +298,7 @@ async function handleSelectionComplete(payload: any) { title: tab.title, }; } catch (error) { - console.warn('Failed to parse URL:', error); + logger.warn('Failed to parse URL', { error }); } } @@ -333,16 +341,16 @@ async function handleSelectionComplete(payload: any) { if (storedAuth?.token) { numbersApi.setAuthToken(storedAuth.token); auth = true; - console.log('✅ Restored auth token from storage'); + logger.info('Restored auth token from storage'); } } if (auth) { await numbersApi.upload.addToQueue(asset); - console.log('✅ Asset added to upload queue'); + logger.info('Asset added to upload queue', { assetId: asset.id }); } } catch (uploadError) { - console.error('Failed to add asset to upload queue:', uploadError); + logger.error('Failed to add asset to upload queue', uploadError); } } @@ -369,7 +377,7 @@ async function handleSelectionComplete(payload: any) { pendingSelectionFromPopup = false; } } catch (error: any) { - console.error('Failed to capture selection:', error); + logger.error('Failed to capture selection', error); if (pendingSelectionReject) { pendingSelectionReject(error); pendingSelectionResolve = null; @@ -456,12 +464,12 @@ async function handleScreenshotCapture( if (response.success) { dataUrl = response.data.dataUrl; - console.log('✅ Watermark added successfully'); + logger.info('Watermark added successfully'); } else { - console.warn('Failed to add watermark:', response.error); + logger.warn('Failed to add watermark', { error: response.error }); } } catch (error) { - console.error('Watermark error:', error); + logger.error('Watermark error', error); // Continue without watermark if it fails } @@ -475,12 +483,12 @@ async function handleScreenshotCapture( }); if (locationResponse.success && locationResponse.data) { gpsLocation = locationResponse.data; - console.log('✅ Geolocation captured:', gpsLocation!.latitude, gpsLocation!.longitude); + logger.info('Geolocation captured', { latitude: gpsLocation!.latitude, longitude: gpsLocation!.longitude }); } else { - console.warn('⚠️ Could not get geolocation:', locationResponse.error || 'Permission denied or unavailable'); + logger.warn('Could not get geolocation', { error: locationResponse.error || 'Permission denied or unavailable' }); } } catch (error) { - console.warn('⚠️ Geolocation error:', error); + logger.warn('Geolocation error', { error }); // Continue without location } } @@ -494,9 +502,9 @@ async function handleScreenshotCapture( url: tab.url, title: tab.title, }; - console.log('✅ Website metadata captured:', url.hostname); + logger.debug('Website metadata captured', { hostname: url.hostname }); } catch (error) { - console.warn('Failed to parse URL:', error); + logger.warn('Failed to parse URL', { error }); } } @@ -522,7 +530,7 @@ async function handleScreenshotCapture( await assetStorage.setAsset(asset); // Note: mode and options parameters preserved for future implementation - console.log('Capture mode:', mode, 'Options:', options); + logger.debug('Capture complete', { mode, assetId }); // Notify popup of new screenshot chrome.runtime.sendMessage({ @@ -552,18 +560,18 @@ async function handleScreenshotCapture( if (storedAuth?.token) { numbersApi.setAuthToken(storedAuth.token); auth = true; - console.log('✅ Restored auth token from storage'); + logger.info('Restored auth token from storage'); } } if (auth) { await numbersApi.upload.addToQueue(asset); - console.log('✅ Asset added to upload queue'); + logger.info('Asset added to upload queue', { assetId }); } else { - console.log('⚠️ Auto-upload enabled but user not authenticated'); + logger.warn('Auto-upload enabled but user not authenticated'); } } catch (uploadError) { - console.error('Failed to add asset to upload queue:', uploadError); + logger.error('Failed to add asset to upload queue', uploadError); // Don't fail the capture if upload queueing fails } } @@ -575,7 +583,7 @@ async function handleScreenshotCapture( autoUpload: settings.autoUpload, }; } catch (error) { - console.error('Screenshot capture failed:', error); + logger.error('Screenshot capture failed', error); throw error; } } @@ -584,10 +592,10 @@ async function handleScreenshotCapture( * Show browser notification for successful capture */ async function showCaptureNotification(autoUpload: boolean) { - console.log('🔔 Attempting to show notification, autoUpload:', autoUpload); + logger.debug('Attempting to show notification', { autoUpload }); try { - console.log('✅ Creating Chrome notification...'); + logger.debug('Creating Chrome notification'); const notificationOptions = { type: 'basic' as const, iconUrl: chrome.runtime.getURL('icons/icon48.png'), @@ -601,20 +609,20 @@ async function showCaptureNotification(autoUpload: boolean) { const notificationId = `proofsnap-${Date.now()}`; chrome.notifications.create(notificationId, notificationOptions, (createdId) => { if (chrome.runtime.lastError) { - console.error('❌ Notification creation error:', chrome.runtime.lastError); + logger.error('Notification creation error', chrome.runtime.lastError); } else { - console.log('✅ Notification created:', createdId); + logger.debug('Notification created', { notificationId: createdId }); // Auto-clear after 3 seconds setTimeout(() => { chrome.notifications.clear(notificationId); - console.log('🔔 Notification cleared'); + logger.debug('Notification cleared', { notificationId }); }, 3000); } }); - console.log('✅ Notification shown'); + logger.debug('Notification shown'); } catch (error) { - console.error('❌ Failed to show notification:', error); + logger.error('Failed to show notification', error); } } @@ -633,7 +641,7 @@ async function updateExtensionBadge() { chrome.action.setBadgeText({ text: '' }); // Clear badge } } catch (error) { - console.warn('Failed to update badge:', error); + logger.warn('Failed to update badge', { error }); } } @@ -651,9 +659,9 @@ async function handleAssetUpload(payload: any) { const numbersApi = await getNumbersApi(); // Pass isManualRetry=true since this is triggered by user clicking retry await numbersApi.upload.addToQueue(asset, true); - console.log('Asset queued for manual retry upload:', asset.id); + logger.info('Asset queued for manual retry upload', { assetId: asset.id }); } catch (error) { - console.error('Failed to queue asset for upload:', error); + logger.error('Failed to queue asset for upload', error); throw error; } } @@ -662,7 +670,7 @@ async function handleAssetUpload(payload: any) { * Keep service worker alive */ self.addEventListener('activate', (_event) => { - console.log('Service worker activated'); + logger.info('Service worker activated'); }); // Export for testing (if needed) diff --git a/src/config/environment.ts b/src/config/environment.ts index bb9ed2e..2dfb4b8 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -17,14 +17,18 @@ export const config: EnvironmentConfig = { }; /** - * Debug helper + * Debug helper — logs environment configuration at INFO level. + * Import the logger lazily to avoid a circular-dependency edge case. */ export function logEnvironmentInfo() { if (config.enableLogging) { - console.log('🌍 Environment Info:', { - apiUrl: config.apiUrl, - enableLogging: config.enableLogging, - timeout: config.timeout, + import('../utils/logger').then(({ createLogger }) => { + const logger = createLogger('Environment'); + logger.info('Environment Info', { + apiUrl: config.apiUrl, + enableLogging: config.enableLogging, + timeout: config.timeout, + }); }); } } diff --git a/src/offscreen/offscreen.ts b/src/offscreen/offscreen.ts index f01a2a0..36b33b2 100644 --- a/src/offscreen/offscreen.ts +++ b/src/offscreen/offscreen.ts @@ -3,7 +3,11 @@ * Used for adding watermarks to screenshots and cropping */ -console.log('ProofSnap offscreen document loaded'); +import { createLogger } from '../utils/logger'; + +const logger = createLogger('Offscreen'); + +logger.info('ProofSnap offscreen document loaded'); // Type definitions for watermark options interface WatermarkPayload { @@ -68,7 +72,7 @@ async function getGeolocation(): Promise<{ latitude: number; longitude: number; }); }, (error) => { - console.warn('Geolocation error:', error.message); + logger.warn('Geolocation error', { message: error.message }); // Don't reject - just return null so capture continues resolve(null); }, @@ -321,7 +325,7 @@ async function drawLogo( ctx.drawImage(logo, logoX, logoY, logoWidth, logoHeight); ctx.globalAlpha = 1.0; } catch (error) { - console.warn('Failed to load logo:', error); + logger.warn('Failed to load logo', { error }); } } diff --git a/src/options/options.tsx b/src/options/options.tsx index ff2bb56..203cd49 100644 --- a/src/options/options.tsx +++ b/src/options/options.tsx @@ -6,8 +6,11 @@ import { useEffect, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { storageService, StoredSettings } from '../services/StorageService'; +import { createLogger } from '../utils/logger'; import './options.css'; +const logger = createLogger('Options'); + /** * Watermark Settings Component */ @@ -463,7 +466,7 @@ function OptionsApp() { const storedSettings = await storageService.getSettings(); setSettings(storedSettings); } catch (error) { - console.error('Failed to load settings:', error); + logger.error('Failed to load settings', error); } finally { setLoading(false); } @@ -484,7 +487,7 @@ function OptionsApp() { setTimeout(() => savedMessage.classList.remove('show'), 2000); } } catch (error) { - console.error('Failed to save settings:', error); + logger.error('Failed to save settings', error); alert('Failed to save settings'); } } diff --git a/src/popup/AuthForm.tsx b/src/popup/AuthForm.tsx index f0f51ab..15916d2 100644 --- a/src/popup/AuthForm.tsx +++ b/src/popup/AuthForm.tsx @@ -1,6 +1,9 @@ import React, { useState, useEffect } from 'react'; import { getNumbersApi } from '../services/NumbersApiManager'; import { storageService } from '../services/StorageService'; +import { createLogger } from '../utils/logger'; + +const logger = createLogger('AuthForm'); const AuthForm: React.FC<{ onLogin: () => void }> = ({ onLogin }) => { const [isLoginMode, setIsLoginMode] = useState(true); @@ -33,7 +36,7 @@ const AuthForm: React.FC<{ onLogin: () => void }> = ({ onLogin }) => { } onLogin(); } catch (err: any) { - console.error('Auth error:', err); + logger.error('Auth error', err); setError(err.message || (isLoginMode ? 'Login failed.' : 'Signup failed.')); } finally { setLoading(false); @@ -66,7 +69,7 @@ const AuthForm: React.FC<{ onLogin: () => void }> = ({ onLogin }) => { // so the user will simply re-open the popup and be logged in. onLogin(); } catch (err: any) { - console.error('Google Auth error:', err); + logger.error('Google Auth error', err); // If popup was closed during auth, this error won't be seen by user, // but it's good for debugging if inspecting. setError(err.message || 'Google Authentication failed.'); diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index 73d7c2f..05b1730 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -10,8 +10,11 @@ import { storageService } from '../services/StorageService'; import AuthForm from './AuthForm'; import InsufficientCreditsNotification from './InsufficientCreditsNotification'; import { getNumbersApi } from '../services/NumbersApiManager'; +import { createLogger } from '../utils/logger'; import './popup.css'; +const logger = createLogger('Popup'); + /** * Hunt Mode settings interface for popup */ @@ -70,7 +73,7 @@ function PopupApp() { // Load Hunt Mode settings const settings = await storageService.getSettings(); const huntModeActive = settings.huntModeEnabled; - console.log('[Hunt Mode Popup] Settings:', { huntModeEnabled: settings.huntModeEnabled, huntModeActive }); + logger.debug('Hunt Mode settings loaded', { huntModeEnabled: settings.huntModeEnabled, huntModeActive }); setHuntMode({ enabled: huntModeActive, message: settings.huntModeMessage, @@ -80,7 +83,7 @@ function PopupApp() { // Check for pending share prompt (from upload that completed while popup was closed) if (huntModeActive) { const pendingNid = await storageService.getAndClearPendingShare(); - console.log('[Hunt Mode Popup] Pending NID:', pendingNid); + logger.debug('Hunt Mode pending NID', { pendingNid }); if (pendingNid) { setSharePromptAsset({ id: 'pending', @@ -95,7 +98,7 @@ function PopupApp() { // Check for insufficient credits error await checkCreditStatus(assets); } catch (error) { - console.error('Failed to load data:', error); + logger.error('Failed to load data', error); } finally { setIsLoading(false); } @@ -113,7 +116,7 @@ function PopupApp() { }); if (response.success) { - console.log('Screenshot captured:', response.data); + logger.info('Screenshot captured', { assetId: response.data?.assetId }); // Reload assets from IndexedDB const assets = await indexedDBService.getAllAssets(); setAssets(assets); @@ -128,13 +131,13 @@ function PopupApp() { } } else if (response.cancelled) { // User cancelled selection - do nothing - console.log('Screenshot cancelled'); + logger.debug('Screenshot cancelled'); } else { - console.error('Capture failed:', response.error); + logger.error('Capture failed', undefined, { error: response.error }); alert('Failed to capture screenshot: ' + response.error); } } catch (error) { - console.error('Capture error:', error); + logger.error('Capture error', error); alert('Failed to capture screenshot'); } finally { setCapturing(false); @@ -149,13 +152,13 @@ function PopupApp() { }); if (response.success) { - console.log('Asset queued for upload'); + logger.info('Asset queued for upload'); // No need to reload - UPLOAD_PROGRESS listener will handle it } else { alert('Failed to queue upload: ' + response.error); } } catch (error) { - console.error('Upload error:', error); + logger.error('Upload error', error); alert('Failed to upload asset'); } } @@ -637,7 +640,7 @@ function AssetThumbnail({ asset, onUpload, huntMode }: { asset: Asset; onUpload? await navigator.clipboard.writeText(verifyUrl); // Could add toast notification here } catch (err) { - console.error('Failed to copy:', err); + logger.error('Failed to copy', err); } } }; @@ -877,7 +880,7 @@ function SharePromptModal({ await navigator.clipboard.writeText(verifyUrl); onClose(); } catch (err) { - console.error('Failed to copy:', err); + logger.error('Failed to copy', err); } }; diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index 25ffe09..5ee9a85 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -3,6 +3,9 @@ * Handles user login, signup, and token management */ import { ApiClient } from './ApiClient'; +import { createLogger } from '../utils/logger'; + +const logger = createLogger('AuthService'); type LoginRequest = { email: string; password: string }; type LoginResponse = { auth_token: string }; @@ -123,7 +126,7 @@ export class AuthService { if (idToken) { resolve(idToken); } else { - console.error('No id_token found in response', responseUrl); + logger.error('No id_token found in response', undefined, { responseUrl }); reject('Failed to retrieve ID token from Google'); } } diff --git a/src/services/NumbersApiManager.ts b/src/services/NumbersApiManager.ts index d95aa29..67c22ac 100644 --- a/src/services/NumbersApiManager.ts +++ b/src/services/NumbersApiManager.ts @@ -9,6 +9,9 @@ import { AuthService } from './AuthService'; import { indexedDBService } from './IndexedDBService'; import { storageService } from './StorageService'; import { UploadService } from './UploadService'; +import { createLogger } from '../utils/logger'; + +const logger = createLogger('NumbersApiManager'); export class NumbersApiManager { private apiClient: ApiClient; @@ -122,7 +125,7 @@ export class NumbersApiManager { username: user.username, }); - console.log('Token validated successfully for user:', user.email); + logger.info('Token validated successfully', { email: user.email }); } catch (error: unknown) { const statusCode = error instanceof ApiError ? error.statusCode : undefined; const errorMessage = error instanceof Error ? error.message : String(error); @@ -134,15 +137,15 @@ export class NumbersApiManager { if (isNetworkError || isServerError) { // Network or server error - keep the token and use cached user data - console.warn('Network/server error during token validation, keeping cached auth:', errorMessage); + logger.warn('Network/server error during token validation, keeping cached auth', { errorMessage }); } else { // Authentication error (401, 403, etc.) - token is invalid, clear it - console.warn('Token validation failed, clearing authentication:', errorMessage); + logger.warn('Token validation failed, clearing authentication', { errorMessage }); await this.clearAuth(); } } } catch (error: unknown) { - console.error('Failed to initialize authentication:', error); + logger.error('Failed to initialize authentication', error); } } } diff --git a/src/services/ScreenshotService.ts b/src/services/ScreenshotService.ts index 7c61272..f0bcf3a 100644 --- a/src/services/ScreenshotService.ts +++ b/src/services/ScreenshotService.ts @@ -4,6 +4,9 @@ */ import { ScreenshotOptions, ScreenshotResult, SelectionCoordinates } from '@/types'; +import { createLogger } from '@/utils/logger'; + +const logger = createLogger('ScreenshotService'); /** * Screenshot Service @@ -80,7 +83,7 @@ export class ScreenshotService { async captureFullPage(options: Partial = {}): Promise { // For now, just capture visible tab // TODO: Implement full page capture with scrolling - console.warn('Full page capture not yet implemented, capturing visible area'); + logger.warn('Full page capture not yet implemented, capturing visible area'); return await this.captureVisibleTab(options); } @@ -261,7 +264,7 @@ export class ScreenshotService { navigator.geolocation.getCurrentPosition( (position) => resolve(position), (error) => { - console.warn('Geolocation error:', error); + logger.warn('Geolocation error', { error }); resolve(null); }, { diff --git a/src/services/UploadService.ts b/src/services/UploadService.ts index 21dc47c..011d62e 100644 --- a/src/services/UploadService.ts +++ b/src/services/UploadService.ts @@ -7,6 +7,9 @@ import { ApiClient } from './ApiClient'; import { storageService } from './StorageService'; import { indexedDBService } from './IndexedDBService'; import type { Asset } from './IndexedDBService'; +import { createLogger } from '../utils/logger'; + +const logger = createLogger('UploadService'); export interface UploadProgress { assetId: string; @@ -50,12 +53,12 @@ export class UploadService { } } this.uploadQueue = assets; - console.log(`Restored ${assets.length} assets to upload queue`); + logger.info(`Restored ${assets.length} assets to upload queue`); // Auto-start processing if not paused this.processQueue(); } } catch (error) { - console.error('Failed to restore upload queue:', error); + logger.error('Failed to restore upload queue', error); } } @@ -68,7 +71,7 @@ export class UploadService { // Check if already in queue const exists = this.uploadQueue.find(a => a.id === asset.id); if (exists) { - console.log('Asset already in upload queue:', asset.id); + logger.debug('Asset already in upload queue', { assetId: asset.id }); return; } @@ -98,7 +101,7 @@ export class UploadService { if (addedCount > 0) { await this.saveQueue(); - console.log(`Added ${addedCount} assets to upload queue`); + logger.info(`Added ${addedCount} assets to upload queue`); // Unpause queue if this is a manual retry this.maybeUnpauseForManualRetry(isManualRetry); @@ -113,7 +116,7 @@ export class UploadService { */ private maybeUnpauseForManualRetry(isManualRetry: boolean): void { if (isManualRetry && this.isPaused) { - console.log('Unpausing upload queue for manual retry'); + logger.info('Unpausing upload queue for manual retry'); this.setPaused(false); } } @@ -188,7 +191,7 @@ export class UploadService { try { callback(assetId); } catch (error) { - console.error('Error in completion callback:', error); + logger.error('Error in completion callback', error); } }); @@ -220,7 +223,7 @@ export class UploadService { try { await this.uploadAsset(asset); } catch (error) { - console.error('Upload failed:', error); + logger.error('Upload failed', error); await this.handleUploadError(asset, error); } @@ -233,7 +236,7 @@ export class UploadService { * Upload a single asset */ private async uploadAsset(asset: Asset): Promise { - console.log('Starting upload for asset:', asset.id); + logger.info('Starting upload for asset', { assetId: asset.id }); await this.updateAssetStatusToUploading(asset); const progressInterval = this.startProgressSimulation(asset); @@ -243,7 +246,7 @@ export class UploadService { const result = await this.apiClient.postWithAuth('/assets/', formData); clearInterval(progressInterval); - console.log('Upload successful:', result); + logger.info('Upload successful', { assetId: asset.id, resultId: result?.id }); await this.handleUploadSuccess(asset, result); } catch (error) { @@ -328,7 +331,7 @@ export class UploadService { private async handleUploadSuccess(asset: Asset, result: any): Promise { // The API returns 'id' which is the same as the nid/cid const nid = result.id || result.cid || result.nid; - console.log('[UploadService] handleUploadSuccess called with nid:', nid); + logger.debug('handleUploadSuccess called', { nid }); // Update asset with uploaded status and metadata asset.status = 'uploaded'; @@ -349,23 +352,21 @@ export class UploadService { if (nid) { try { const settings = await this.metadataStorage.getSettings(); - console.log('[Hunt Mode] Settings loaded:', { - huntModeEnabled: settings.huntModeEnabled - }); + logger.debug('Hunt Mode settings loaded', { huntModeEnabled: settings.huntModeEnabled }); if (settings.huntModeEnabled) { - console.log('[Hunt Mode] Opening share page for nid:', nid); + logger.info('Hunt Mode: opening share page', { nid }); // Open share page in new tab const shareUrl = chrome.runtime.getURL(`share.html?nid=${nid}`); - console.log('[Hunt Mode] Share URL:', shareUrl); + logger.debug('Hunt Mode share URL', { shareUrl }); await chrome.tabs.create({ url: shareUrl, active: true, }); - console.log('[Hunt Mode] Tab created successfully'); + logger.debug('Hunt Mode: tab created successfully'); } } catch (error) { - console.error('[Hunt Mode] Error opening share page:', error); + logger.error('Hunt Mode: error opening share page', error); } } @@ -378,7 +379,7 @@ export class UploadService { // Clean up: delete uploaded asset from IndexedDB to save disk space await this.assetStorage.deleteAsset(asset.id); - console.log('Deleted uploaded asset from local storage:', asset.id); + logger.info('Deleted uploaded asset from local storage', { assetId: asset.id }); // Notify completion this.emitCompletion(asset.id); @@ -393,7 +394,7 @@ export class UploadService { // Check for insufficient balance let errorType; if (this.isInsufficientBalanceError(error)) { - console.warn('Insufficient balance detected, pausing uploads'); + logger.warn('Insufficient balance detected, pausing uploads'); this.setPaused(true); errorType = 'insufficient_credits'; @@ -493,7 +494,7 @@ export class UploadService { const failedAssets = assets.filter(a => a.status === 'failed'); if (failedAssets.length > 0) { - console.log(`Retrying ${failedAssets.length} failed uploads`); + logger.info(`Retrying ${failedAssets.length} failed uploads`); // Pass isManualRetry=true to unpause queue if it was paused await this.addMultipleToQueue(failedAssets, true); } diff --git a/src/share/share.tsx b/src/share/share.tsx index 3bdd33a..a2ea197 100644 --- a/src/share/share.tsx +++ b/src/share/share.tsx @@ -4,8 +4,11 @@ */ import { storageService } from '../services/StorageService'; +import { createLogger } from '../utils/logger'; import './share.css'; +const logger = createLogger('Share'); + async function init() { // Get nid from URL params const params = new URLSearchParams(window.location.search); @@ -72,7 +75,7 @@ async function init() { toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2000); } catch (err) { - console.error('Failed to copy:', err); + logger.error('Failed to copy', err); } }); diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..e8a0ee6 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,93 @@ +/** + * Structured Logger for ProofSnap Extension + * + * Provides levelled, module-scoped logging with timestamps. + * - Development builds: DEBUG level (all messages shown) + * - Production builds: INFO level (debug messages suppressed) + * + * Usage: + * import { createLogger } from '@/utils/logger'; + * const logger = createLogger('MyModule'); + * logger.debug('Verbose detail', { key: 'value' }); + * logger.info('Something happened'); + * logger.warn('Unexpected condition', { detail }); + * logger.error('Operation failed', error, { assetId }); + */ + +export enum LogLevel { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, +} + +const LEVEL_LABELS: Record = { + [LogLevel.DEBUG]: 'DEBUG', + [LogLevel.INFO]: 'INFO', + [LogLevel.WARN]: 'WARN', + [LogLevel.ERROR]: 'ERROR', +}; + +/** + * Minimum log level. + * In development (Vite dev / watch mode) all levels are shown. + * In production builds only INFO and above are shown. + */ +const MIN_LEVEL: LogLevel = import.meta.env.DEV ? LogLevel.DEBUG : LogLevel.INFO; + +export class Logger { + constructor(private readonly module: string) {} + + debug(msg: string, context?: Record): void { + this.emit(LogLevel.DEBUG, msg, undefined, context); + } + + info(msg: string, context?: Record): void { + this.emit(LogLevel.INFO, msg, undefined, context); + } + + warn(msg: string, context?: Record): void { + this.emit(LogLevel.WARN, msg, undefined, context); + } + + error(msg: string, error?: unknown, context?: Record): void { + this.emit(LogLevel.ERROR, msg, error, context); + } + + private emit( + level: LogLevel, + msg: string, + error?: unknown, + context?: Record, + ): void { + if (level < MIN_LEVEL) return; + + const timestamp = new Date().toISOString(); + const label = LEVEL_LABELS[level]; + const prefix = `[${timestamp}] [${label}] [${this.module}]`; + const parts: unknown[] = [`${prefix} ${msg}`]; + + if (context !== undefined) parts.push(context); + if (error !== undefined) parts.push(error); + + switch (level) { + case LogLevel.DEBUG: + case LogLevel.INFO: + console.log(...parts); + break; + case LogLevel.WARN: + console.warn(...parts); + break; + case LogLevel.ERROR: + console.error(...parts); + break; + } + } +} + +/** + * Factory function — creates a Logger scoped to the given module name. + */ +export function createLogger(module: string): Logger { + return new Logger(module); +} From c740a0ceac5fa41758dde9b42b0a1061eb538872 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:22:07 +0000 Subject: [PATCH 3/3] Address code review feedback: clean up error() calls and add .catch() to dynamic import Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com> --- src/config/environment.ts | 23 ++++++++++++++++------- src/popup/popup.tsx | 2 +- src/services/AuthService.ts | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/config/environment.ts b/src/config/environment.ts index 2dfb4b8..f087eb7 100644 --- a/src/config/environment.ts +++ b/src/config/environment.ts @@ -22,13 +22,22 @@ export const config: EnvironmentConfig = { */ export function logEnvironmentInfo() { if (config.enableLogging) { - import('../utils/logger').then(({ createLogger }) => { - const logger = createLogger('Environment'); - logger.info('Environment Info', { - apiUrl: config.apiUrl, - enableLogging: config.enableLogging, - timeout: config.timeout, + import('../utils/logger') + .then(({ createLogger }) => { + const logger = createLogger('Environment'); + logger.info('Environment Info', { + apiUrl: config.apiUrl, + enableLogging: config.enableLogging, + timeout: config.timeout, + }); + }) + .catch(() => { + // Fallback if logger module fails to load + console.info('[Environment] Environment Info:', { + apiUrl: config.apiUrl, + enableLogging: config.enableLogging, + timeout: config.timeout, + }); }); - }); } } diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index 05b1730..df58b20 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -133,7 +133,7 @@ function PopupApp() { // User cancelled selection - do nothing logger.debug('Screenshot cancelled'); } else { - logger.error('Capture failed', undefined, { error: response.error }); + logger.error('Capture failed', new Error(response.error)); alert('Failed to capture screenshot: ' + response.error); } } catch (error) { diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index 5ee9a85..484f6bc 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -126,7 +126,7 @@ export class AuthService { if (idToken) { resolve(idToken); } else { - logger.error('No id_token found in response', undefined, { responseUrl }); + logger.error('No id_token found in response', { responseUrl }); reject('Failed to retrieve ID token from Google'); } }