Add more logging and analytics to updater processes (#6208)
This commit is contained in:
@@ -172,7 +172,55 @@ export type AnalyticsEvent =
|
||||
| { name: 'input_mode_changed'; properties: { from_mode: string; to_mode: string } }
|
||||
| { name: 'input_diagnostics_opened'; properties: Record<string, never> }
|
||||
| { name: 'input_create_recipe_opened'; properties: Record<string, never> }
|
||||
| { name: 'input_edit_recipe_opened'; properties: Record<string, never> };
|
||||
| { name: 'input_edit_recipe_opened'; properties: Record<string, never> }
|
||||
// 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<T extends AnalyticsEvent>(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 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
const timeoutError = setTimeout(() => {
|
||||
log.error(
|
||||
`Update check appears stuck - no response after 60 seconds (started at ${new Date(checkStartTime).toISOString()})`
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
updateAvailable = true;
|
||||
lastUpdateState = { updateAvailable: true, latestVersion: result.latestVersion };
|
||||
updateTrayIcon(true);
|
||||
sendStatusToWindow('update-available', { version: result.latestVersion });
|
||||
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,
|
||||
});
|
||||
|
||||
// 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,
|
||||
// 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<void> {
|
||||
// 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
|
||||
|
||||
@@ -32,20 +32,32 @@ export class GitHubUpdater {
|
||||
private readonly apiUrl = `https://api.github.com/repos/${this.owner}/${this.repo}/releases/latest`;
|
||||
|
||||
async checkForUpdates(): Promise<UpdateCheckResult> {
|
||||
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',
|
||||
|
||||
Reference in New Issue
Block a user