diff --git a/ui/desktop/src/utils/analytics.ts b/ui/desktop/src/utils/analytics.ts index 2b8540f6ff23..7b9361a2fb74 100644 --- a/ui/desktop/src/utils/analytics.ts +++ b/ui/desktop/src/utils/analytics.ts @@ -172,7 +172,55 @@ export type AnalyticsEvent = | { name: 'input_mode_changed'; properties: { from_mode: string; to_mode: string } } | { name: 'input_diagnostics_opened'; properties: Record } | { name: 'input_create_recipe_opened'; properties: Record } - | { name: 'input_edit_recipe_opened'; properties: Record }; + | { name: 'input_edit_recipe_opened'; properties: Record } + // Auto-update tracking events + | { + name: 'update_check_started'; + properties: { trigger: 'startup' | 'manual'; current_version: string }; + } + | { + name: 'update_check_completed'; + properties: { + result: 'available' | 'not_available' | 'error'; + current_version: string; + latest_version?: string; + using_fallback: boolean; + error_type?: string; + }; + } + | { + name: 'update_download_started'; + properties: { + version: string; + method: 'electron-updater' | 'github-fallback'; + }; + } + | { + name: 'update_download_progress'; + properties: { + milestone: 25 | 50 | 75 | 100; + version: string; + method: 'electron-updater' | 'github-fallback'; + }; + } + | { + name: 'update_download_completed'; + properties: { + success: boolean; + version: string; + method: 'electron-updater' | 'github-fallback'; + duration_seconds?: number; + error_type?: string; + }; + } + | { + name: 'update_install_initiated'; + properties: { + version: string; + method: 'electron-updater' | 'github-fallback'; + action: 'quit_and_install' | 'open_folder_and_quit' | 'open_folder_only'; + }; + }; // NOTE: slash_command_used is tracked by the backend (posthog.rs) with command_type info export function trackEvent(event: T): void { @@ -564,3 +612,115 @@ export function trackEditRecipeOpened(): void { properties: {}, }); } + +// ============================================================================ +// Auto-Update Tracking +// ============================================================================ + +type UpdateMethod = 'electron-updater' | 'github-fallback'; + +let updateDownloadStartTime: number | null = null; +let currentUpdateVersion: string | null = null; +let currentUpdateMethod: UpdateMethod | null = null; +let reportedMilestones: Set<25 | 50 | 75 | 100> = new Set(); + +export function trackUpdateCheckStarted( + trigger: 'startup' | 'manual', + currentVersion: string +): void { + trackEvent({ + name: 'update_check_started', + properties: { trigger, current_version: currentVersion }, + }); +} + +export function trackUpdateCheckCompleted( + result: 'available' | 'not_available' | 'error', + currentVersion: string, + options: { + latestVersion?: string; + usingFallback: boolean; + errorType?: string; + } +): void { + trackEvent({ + name: 'update_check_completed', + properties: { + result, + current_version: currentVersion, + latest_version: options.latestVersion, + using_fallback: options.usingFallback, + error_type: options.errorType, + }, + }); +} + +export function trackUpdateDownloadStarted(version: string, method: UpdateMethod): void { + updateDownloadStartTime = Date.now(); + currentUpdateVersion = version; + currentUpdateMethod = method; + reportedMilestones = new Set(); + + trackEvent({ + name: 'update_download_started', + properties: { version, method }, + }); +} + +export function trackUpdateDownloadProgress(percent: number): void { + if (!currentUpdateVersion || !currentUpdateMethod) return; + + const milestones: Array<25 | 50 | 75 | 100> = [25, 50, 75, 100]; + for (const milestone of milestones) { + if (percent >= milestone && !reportedMilestones.has(milestone)) { + reportedMilestones.add(milestone); + trackEvent({ + name: 'update_download_progress', + properties: { + milestone, + version: currentUpdateVersion, + method: currentUpdateMethod, + }, + }); + } + } +} + +export function trackUpdateDownloadCompleted( + success: boolean, + version: string, + method: UpdateMethod, + errorType?: string +): void { + const durationSeconds = updateDownloadStartTime + ? Math.round((Date.now() - updateDownloadStartTime) / 1000) + : undefined; + + trackEvent({ + name: 'update_download_completed', + properties: { + success, + version, + method, + duration_seconds: durationSeconds, + error_type: errorType, + }, + }); + + // Reset state + updateDownloadStartTime = null; + currentUpdateVersion = null; + currentUpdateMethod = null; + reportedMilestones = new Set(); +} + +export function trackUpdateInstallInitiated( + version: string, + method: UpdateMethod, + action: 'quit_and_install' | 'open_folder_and_quit' | 'open_folder_only' +): void { + trackEvent({ + name: 'update_install_initiated', + properties: { version, method, action }, + }); +} diff --git a/ui/desktop/src/utils/autoUpdater.ts b/ui/desktop/src/utils/autoUpdater.ts index 11419fb88cdf..2694ed649df4 100644 --- a/ui/desktop/src/utils/autoUpdater.ts +++ b/ui/desktop/src/utils/autoUpdater.ts @@ -16,6 +16,14 @@ import * as fs from 'fs/promises'; import log from './logger'; import { githubUpdater } from './githubUpdater'; import { loadRecentDirs } from './recentDirs'; +import { + trackUpdateCheckStarted, + trackUpdateCheckCompleted, + trackUpdateDownloadStarted, + trackUpdateDownloadProgress, + trackUpdateDownloadCompleted, + trackUpdateInstallInitiated, +} from './analytics'; let updateAvailable = false; let trayRef: Tray | null = null; @@ -48,8 +56,14 @@ export function registerUpdateIpcHandlers() { // IPC handlers for renderer process ipcMain.handle('check-for-updates', async () => { + const currentVersion = autoUpdater.currentVersion?.version || app.getVersion(); + const checkStartTime = Date.now(); + try { - log.info('Manual check for updates requested'); + log.info('=== MANUAL UPDATE CHECK INITIATED ==='); + log.info(`Manual check for updates requested at ${new Date().toISOString()}`); + log.info(`Current version: ${currentVersion}`); + trackUpdateCheckStarted('manual', currentVersion); // Reset state for new update check isUsingGitHubFallback = false; @@ -59,6 +73,10 @@ export function registerUpdateIpcHandlers() { // Ensure auto-updater is properly initialized if (!autoUpdater.currentVersion) { log.error('Auto-updater currentVersion is null/undefined'); + trackUpdateCheckCompleted('error', currentVersion, { + usingFallback: false, + errorType: 'auto_updater_not_initialized', + }); throw new Error('Auto-updater not initialized. Please restart the application.'); } @@ -68,6 +86,8 @@ export function registerUpdateIpcHandlers() { log.info(`Feed URL: ${autoUpdater.getFeedURL()}`); const result = await autoUpdater.checkForUpdates(); + const duration = Date.now() - checkStartTime; + log.info(`=== MANUAL UPDATE CHECK COMPLETED in ${duration}ms ===`); log.info('Auto-updater checkForUpdates result:', result); return { @@ -75,6 +95,8 @@ export function registerUpdateIpcHandlers() { error: null, }; } catch (error) { + const duration = Date.now() - checkStartTime; + log.error(`=== MANUAL UPDATE CHECK FAILED after ${duration}ms ===`); log.error('Error checking for updates:', error); log.error('Manual check error details:', { message: error instanceof Error ? error.message : 'Unknown error', @@ -103,6 +125,10 @@ export function registerUpdateIpcHandlers() { const result = await githubUpdater.checkForUpdates(); if (result.error) { + trackUpdateCheckCompleted('error', currentVersion, { + usingFallback: true, + errorType: result.error, + }); return { updateInfo: null, error: result.error, @@ -117,6 +143,11 @@ export function registerUpdateIpcHandlers() { releaseUrl: result.releaseUrl, }; + trackUpdateCheckCompleted('available', currentVersion, { + latestVersion: result.latestVersion, + usingFallback: true, + }); + updateAvailable = true; lastUpdateState = { updateAvailable: true, latestVersion: result.latestVersion }; updateTrayIcon(true); @@ -126,6 +157,11 @@ export function registerUpdateIpcHandlers() { log.info('Auto-downloading update via GitHub fallback...'); await githubAutoDownload(result.downloadUrl!, result.latestVersion!, 'manual check'); } else { + trackUpdateCheckCompleted('not_available', currentVersion, { + latestVersion: result.latestVersion, + usingFallback: true, + }); + updateAvailable = false; lastUpdateState = { updateAvailable: false }; updateTrayIcon(false); @@ -140,6 +176,10 @@ export function registerUpdateIpcHandlers() { }; } catch (fallbackError) { log.error('GitHub fallback also failed:', fallbackError); + trackUpdateCheckCompleted('error', currentVersion, { + usingFallback: true, + errorType: 'github_fallback_failed', + }); return { updateInfo: null, error: 'Unable to check for updates. Please check your internet connection.', @@ -147,6 +187,11 @@ export function registerUpdateIpcHandlers() { } } + trackUpdateCheckCompleted('error', currentVersion, { + usingFallback: false, + errorType: error instanceof Error ? error.message : 'unknown', + }); + return { updateInfo: null, error: error instanceof Error ? error.message : 'Unknown error', @@ -159,6 +204,7 @@ export function registerUpdateIpcHandlers() { if (isUsingGitHubFallback && githubUpdateInfo.downloadUrl && githubUpdateInfo.latestVersion) { log.info('Using GitHub fallback for download...'); lastReportedProgress = 0; // Reset progress tracking + trackUpdateDownloadStarted(githubUpdateInfo.latestVersion, 'github-fallback'); const result = await githubUpdater.downloadUpdate( githubUpdateInfo.downloadUrl, @@ -167,6 +213,7 @@ export function registerUpdateIpcHandlers() { // Only send if progress increased (monotonic) if (percent > lastReportedProgress) { lastReportedProgress = percent; + trackUpdateDownloadProgress(percent); sendStatusToWindow('download-progress', { percent }); } } @@ -175,18 +222,36 @@ export function registerUpdateIpcHandlers() { if (result.success && result.downloadPath) { githubUpdateInfo.downloadPath = result.downloadPath; githubUpdateInfo.extractedPath = result.extractedPath; + trackUpdateDownloadCompleted(true, githubUpdateInfo.latestVersion, 'github-fallback'); sendStatusToWindow('update-downloaded', { version: githubUpdateInfo.latestVersion }); return { success: true, error: null }; } else { - throw new Error(result.error || 'Download failed'); + const errorMsg = result.error || 'Download failed'; + trackUpdateDownloadCompleted( + false, + githubUpdateInfo.latestVersion, + 'github-fallback', + errorMsg + ); + throw new Error(errorMsg); } } else { // Use electron-updater + const version = lastUpdateState?.latestVersion || 'unknown'; + trackUpdateDownloadStarted(version, 'electron-updater'); await autoUpdater.downloadUpdate(); return { success: true, error: null }; } } catch (error) { log.error('Error downloading update:', error); + const version = githubUpdateInfo.latestVersion || lastUpdateState?.latestVersion || 'unknown'; + const method = isUsingGitHubFallback ? 'github-fallback' : 'electron-updater'; + trackUpdateDownloadCompleted( + false, + version, + method, + error instanceof Error ? error.message : 'unknown' + ); return { success: false, error: error instanceof Error ? error.message : 'Unknown error', @@ -226,21 +291,37 @@ export function registerUpdateIpcHandlers() { })) as unknown as { response: number }; if (dialogResult.response === 0) { + trackUpdateInstallInitiated( + githubUpdateInfo.latestVersion || 'unknown', + 'github-fallback', + 'open_folder_and_quit' + ); // Open folder and quit app for easy replacement shell.showItemInFolder(updatePath); setTimeout(() => { app.quit(); }, 1500); // Give user time to see the folder open } else if (dialogResult.response === 1) { + trackUpdateInstallInitiated( + githubUpdateInfo.latestVersion || 'unknown', + 'github-fallback', + 'open_folder_only' + ); // Just open folder, don't quit shell.showItemInFolder(updatePath); } + // response === 2 is Cancel, no tracking needed } catch (error) { log.error('Error installing GitHub update:', error); throw error; } } else { // Use electron-updater's built-in install + trackUpdateInstallInitiated( + lastUpdateState?.latestVersion || 'unknown', + 'electron-updater', + 'quit_and_install' + ); autoUpdater.quitAndInstall(false, true); } }); @@ -323,65 +404,122 @@ export function setupAutoUpdater(tray?: Tray) { // Check for updates on startup setTimeout(() => { - log.info('Checking for updates on startup...'); + const currentVersion = autoUpdater.currentVersion?.version || app.getVersion(); + const checkStartTime = Date.now(); + log.info('=== STARTUP UPDATE CHECK INITIATED ==='); + log.info(`Checking for updates on startup at ${new Date().toISOString()}`); log.info(`autoUpdater.currentVersion: ${JSON.stringify(autoUpdater.currentVersion)}`); log.info(`autoUpdater.getFeedURL(): ${autoUpdater.getFeedURL()}`); + log.info( + `Network online status: ${typeof navigator !== 'undefined' ? navigator.onLine : 'unknown'}` + ); - autoUpdater.checkForUpdates().catch((err) => { - log.error('Error checking for updates on startup:', err); - log.error('Error details:', { - message: err.message, - stack: err.stack, - name: err.name, - code: 'code' in err ? err.code : undefined, - }); + trackUpdateCheckStarted('startup', currentVersion); - // If electron-updater fails, try GitHub API as fallback - if ( - err.message.includes('HttpError: 404') || - err.message.includes('ERR_CONNECTION_REFUSED') || - err.message.includes('ENOTFOUND') || - err.message.includes('No published versions') - ) { - log.info('Using GitHub API fallback for startup update check...'); - log.info('Fallback triggered by error containing:', err.message); - isUsingGitHubFallback = true; + // Set up a timeout warning for long-running checks + const timeoutWarning = setTimeout(() => { + log.warn( + `Update check still in progress after 30 seconds (started at ${new Date(checkStartTime).toISOString()})` + ); + }, 30000); - githubUpdater - .checkForUpdates() - .then(async (result) => { - if (result.error) { - sendStatusToWindow('error', result.error); - } else if (result.updateAvailable) { - // Store GitHub update info - githubUpdateInfo = { - latestVersion: result.latestVersion, - downloadUrl: result.downloadUrl, - releaseUrl: result.releaseUrl, - }; - - updateAvailable = true; - lastUpdateState = { updateAvailable: true, latestVersion: result.latestVersion }; - updateTrayIcon(true); - sendStatusToWindow('update-available', { version: result.latestVersion }); - - // Auto-download for GitHub fallback (matching autoDownload behavior) - log.info('Auto-downloading update via GitHub fallback on startup...'); - await githubAutoDownload(result.downloadUrl!, result.latestVersion!, 'on startup'); - } else { - updateAvailable = false; - lastUpdateState = { updateAvailable: false }; - updateTrayIcon(false); - sendStatusToWindow('update-not-available', { - version: autoUpdater.currentVersion.version, + const timeoutError = setTimeout(() => { + log.error( + `Update check appears stuck - no response after 60 seconds (started at ${new Date(checkStartTime).toISOString()})` + ); + }, 60000); + + autoUpdater + .checkForUpdates() + .then((result) => { + clearTimeout(timeoutWarning); + clearTimeout(timeoutError); + const duration = Date.now() - checkStartTime; + log.info(`=== STARTUP UPDATE CHECK COMPLETED in ${duration}ms ===`); + log.info('Update check result:', result); + }) + .catch((err) => { + clearTimeout(timeoutWarning); + clearTimeout(timeoutError); + const duration = Date.now() - checkStartTime; + log.error(`=== STARTUP UPDATE CHECK FAILED after ${duration}ms ===`); + log.error('Error checking for updates on startup:', err); + log.error('Error details:', { + message: err.message, + stack: err.stack, + name: err.name, + code: 'code' in err ? err.code : undefined, + }); + + // If electron-updater fails, try GitHub API as fallback + if ( + err.message.includes('HttpError: 404') || + err.message.includes('ERR_CONNECTION_REFUSED') || + err.message.includes('ENOTFOUND') || + err.message.includes('No published versions') + ) { + log.info('Using GitHub API fallback for startup update check...'); + log.info('Fallback triggered by error containing:', err.message); + isUsingGitHubFallback = true; + + githubUpdater + .checkForUpdates() + .then(async (result) => { + if (result.error) { + trackUpdateCheckCompleted('error', currentVersion, { + usingFallback: true, + errorType: result.error, + }); + sendStatusToWindow('error', result.error); + } else if (result.updateAvailable) { + // Store GitHub update info + githubUpdateInfo = { + latestVersion: result.latestVersion, + downloadUrl: result.downloadUrl, + releaseUrl: result.releaseUrl, + }; + + trackUpdateCheckCompleted('available', currentVersion, { + latestVersion: result.latestVersion, + usingFallback: true, + }); + + updateAvailable = true; + lastUpdateState = { updateAvailable: true, latestVersion: result.latestVersion }; + updateTrayIcon(true); + sendStatusToWindow('update-available', { version: result.latestVersion }); + + // Auto-download for GitHub fallback (matching autoDownload behavior) + log.info('Auto-downloading update via GitHub fallback on startup...'); + await githubAutoDownload(result.downloadUrl!, result.latestVersion!, 'on startup'); + } else { + trackUpdateCheckCompleted('not_available', currentVersion, { + latestVersion: result.latestVersion, + usingFallback: true, + }); + + updateAvailable = false; + lastUpdateState = { updateAvailable: false }; + updateTrayIcon(false); + sendStatusToWindow('update-not-available', { + version: autoUpdater.currentVersion.version, + }); + } + }) + .catch((fallbackError) => { + log.error('GitHub fallback also failed on startup:', fallbackError); + trackUpdateCheckCompleted('error', currentVersion, { + usingFallback: true, + errorType: 'github_fallback_failed', }); - } - }) - .catch((fallbackError) => { - log.error('GitHub fallback also failed on startup:', fallbackError); + }); + } else { + trackUpdateCheckCompleted('error', currentVersion, { + usingFallback: false, + errorType: err.message, }); - } - }); + } + }); }, 5000); // Wait 5 seconds after app starts // Handle update events @@ -394,6 +532,12 @@ export function setupAutoUpdater(tray?: Tray) { autoUpdater.on('update-available', (info: UpdateInfo) => { log.info('Update available:', info); + const currentVersion = autoUpdater.currentVersion?.version || app.getVersion(); + trackUpdateCheckCompleted('available', currentVersion, { + latestVersion: info.version, + usingFallback: false, + }); + trackUpdateDownloadStarted(info.version, 'electron-updater'); updateAvailable = true; lastUpdateState = { updateAvailable: true, latestVersion: info.version }; updateTrayIcon(true); @@ -402,6 +546,11 @@ export function setupAutoUpdater(tray?: Tray) { autoUpdater.on('update-not-available', (info: UpdateInfo) => { log.info('Update not available:', info); + const currentVersion = autoUpdater.currentVersion?.version || app.getVersion(); + trackUpdateCheckCompleted('not_available', currentVersion, { + latestVersion: info.version, + usingFallback: false, + }); updateAvailable = false; lastUpdateState = { updateAvailable: false }; updateTrayIcon(false); @@ -474,6 +623,7 @@ export function setupAutoUpdater(tray?: Tray) { // Only send progress if it increased (prevents backward jumps) if (roundedPercent > lastReportedProgress) { lastReportedProgress = roundedPercent; + trackUpdateDownloadProgress(roundedPercent); const log_message = `Download: ${roundedPercent}% (${progressObj.transferred}/${progressObj.total}) @ ${Math.round(progressObj.bytesPerSecond / 1024)} KB/s`; log.info(log_message); @@ -487,6 +637,7 @@ export function setupAutoUpdater(tray?: Tray) { autoUpdater.on('update-downloaded', (info: UpdateInfo) => { log.info('Update downloaded:', info); + trackUpdateDownloadCompleted(true, info.version, 'electron-updater'); sendStatusToWindow('update-downloaded', info); // Show native notification @@ -498,6 +649,7 @@ export function setupAutoUpdater(tray?: Tray) { // Optional: Add click handler to install immediately notification.on('click', () => { + trackUpdateInstallInitiated(info.version, 'electron-updater', 'quit_and_install'); autoUpdater.quitAndInstall(false, true); }); }); @@ -523,6 +675,7 @@ async function githubAutoDownload( ): Promise { // Reset progress tracking for new download lastReportedProgress = 0; + trackUpdateDownloadStarted(latestVersion, 'github-fallback'); try { const downloadResult = await githubUpdater.downloadUpdate( @@ -532,6 +685,7 @@ async function githubAutoDownload( // Only send if progress increased (monotonic) if (percent > lastReportedProgress) { lastReportedProgress = percent; + trackUpdateDownloadProgress(percent); sendStatusToWindow('download-progress', { percent }); } } @@ -540,14 +694,22 @@ async function githubAutoDownload( if (downloadResult.success && downloadResult.downloadPath) { githubUpdateInfo.downloadPath = downloadResult.downloadPath; githubUpdateInfo.extractedPath = downloadResult.extractedPath; + trackUpdateDownloadCompleted(true, latestVersion, 'github-fallback'); sendStatusToWindow('update-downloaded', { version: latestVersion }); } else { + trackUpdateDownloadCompleted(false, latestVersion, 'github-fallback', downloadResult.error); log.error( `GitHub auto-download failed${contextLabel ? ` (${contextLabel})` : ''}:`, downloadResult.error ); } } catch (downloadError) { + trackUpdateDownloadCompleted( + false, + latestVersion, + 'github-fallback', + downloadError instanceof Error ? downloadError.message : 'unknown' + ); log.error( `Error during GitHub auto-download${contextLabel ? ` (${contextLabel})` : ''}:`, downloadError diff --git a/ui/desktop/src/utils/githubUpdater.ts b/ui/desktop/src/utils/githubUpdater.ts index a80a564610bd..4a87b1ff0673 100644 --- a/ui/desktop/src/utils/githubUpdater.ts +++ b/ui/desktop/src/utils/githubUpdater.ts @@ -32,20 +32,32 @@ export class GitHubUpdater { private readonly apiUrl = `https://api.github.com/repos/${this.owner}/${this.repo}/releases/latest`; async checkForUpdates(): Promise { + const startTime = Date.now(); try { - log.info('GitHubUpdater: Checking for updates via GitHub API...'); + log.info('=== GitHubUpdater: STARTING UPDATE CHECK ==='); log.info(`GitHubUpdater: API URL: ${this.apiUrl}`); log.info(`GitHubUpdater: Current app version: ${app.getVersion()}`); + log.info(`GitHubUpdater: Timestamp: ${new Date().toISOString()}`); + + log.info('GitHubUpdater: Initiating fetch request...'); + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + log.error('GitHubUpdater: Fetch request timed out after 30 seconds'); + controller.abort(); + }, 30000); const response = await fetch(this.apiUrl, { headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': `Goose-Desktop/${app.getVersion()}`, }, + signal: controller.signal, }); + clearTimeout(timeoutId); + const fetchDuration = Date.now() - startTime; log.info( - `GitHubUpdater: GitHub API response status: ${response.status} ${response.statusText}` + `GitHubUpdater: GitHub API response status: ${response.status} ${response.statusText} (took ${fetchDuration}ms)` ); if (!response.ok) { @@ -150,10 +162,20 @@ export class GitHubUpdater { latestVersion: string, onProgress?: (percent: number) => void ): Promise<{ success: boolean; downloadPath?: string; extractedPath?: string; error?: string }> { + const downloadStartTime = Date.now(); try { - log.info(`GitHubUpdater: Downloading update from ${downloadUrl}`); + log.info('=== GitHubUpdater: STARTING DOWNLOAD ==='); + log.info(`GitHubUpdater: Download URL: ${downloadUrl}`); + log.info(`GitHubUpdater: Version: ${latestVersion}`); + log.info(`GitHubUpdater: Timestamp: ${new Date().toISOString()}`); + log.info('GitHubUpdater: Initiating download fetch request...'); const response = await fetch(downloadUrl); + const fetchDuration = Date.now() - downloadStartTime; + log.info( + `GitHubUpdater: Download response received in ${fetchDuration}ms - Status: ${response.status} ${response.statusText}` + ); + if (!response.ok) { throw new Error(`Download failed: ${response.status} ${response.statusText}`); } @@ -161,16 +183,22 @@ export class GitHubUpdater { // Get total size from headers const contentLength = response.headers.get('content-length'); const totalSize = contentLength ? parseInt(contentLength, 10) : 0; + log.info( + `GitHubUpdater: Content-Length: ${totalSize} bytes (${(totalSize / 1024 / 1024).toFixed(2)} MB)` + ); if (!response.body) { throw new Error('Response body is null'); } let lastReportedPercent = -1; // Track last reported percentage to throttle updates + let lastLoggedPercent = -1; // Track for logging at 10% intervals // Read the response stream + log.info('GitHubUpdater: Starting to read response stream...'); const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let downloadedSize = 0; + let lastProgressTime = Date.now(); while (true) { const { done, value } = await reader.read(); @@ -187,27 +215,66 @@ export class GitHubUpdater { if (percent !== lastReportedPercent) { onProgress(percent); lastReportedPercent = percent; + + // Log at 10% intervals for debugging + if (percent % 10 === 0 && percent !== lastLoggedPercent) { + const elapsed = Date.now() - downloadStartTime; + const speed = downloadedSize / (elapsed / 1000) / 1024; // KB/s + log.info( + `GitHubUpdater: Download progress ${percent}% (${(downloadedSize / 1024 / 1024).toFixed(2)}/${(totalSize / 1024 / 1024).toFixed(2)} MB) @ ${speed.toFixed(0)} KB/s` + ); + lastLoggedPercent = percent; + } } } + + // Warn if no progress for 30 seconds + const now = Date.now(); + if (now - lastProgressTime > 30000) { + log.warn( + `GitHubUpdater: Download appears slow - no significant progress in 30 seconds (${downloadedSize}/${totalSize} bytes)` + ); + lastProgressTime = now; + } else if (value.length > 0) { + lastProgressTime = now; + } } + const downloadDuration = Date.now() - downloadStartTime; + const avgSpeed = downloadedSize / (downloadDuration / 1000) / 1024; + log.info( + `GitHubUpdater: Download stream complete - ${downloadedSize} bytes in ${downloadDuration}ms (avg ${avgSpeed.toFixed(0)} KB/s)` + ); + // Combine chunks into a single buffer + log.info('GitHubUpdater: Combining chunks into buffer...'); // eslint-disable-next-line no-undef const buffer = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))); + log.info(`GitHubUpdater: Buffer created - ${buffer.length} bytes`); // Save to Downloads directory const downloadsDir = path.join(os.homedir(), 'Downloads'); const fileName = `goose-${latestVersion}.zip`; const downloadPath = path.join(downloadsDir, fileName); + log.info(`GitHubUpdater: Writing file to ${downloadPath}...`); await fs.writeFile(downloadPath, buffer); - log.info(`GitHubUpdater: Update downloaded to ${downloadPath}`); + const totalDuration = Date.now() - downloadStartTime; + log.info(`=== GitHubUpdater: DOWNLOAD COMPLETE in ${totalDuration}ms ===`); + log.info(`GitHubUpdater: File saved to ${downloadPath}`); // Return success - user will handle extraction manually return { success: true, downloadPath, extractedPath: downloadsDir }; } catch (error) { + const duration = Date.now() - downloadStartTime; + log.error(`=== GitHubUpdater: DOWNLOAD FAILED after ${duration}ms ===`); log.error('GitHubUpdater: Error downloading update:', error); + log.error('GitHubUpdater: Download error details:', { + message: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : 'No stack', + name: error instanceof Error ? error.name : 'Unknown', + }); return { success: false, error: error instanceof Error ? error.message : 'Unknown error',