diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a2e3832..1acb6b1 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -29,8 +29,9 @@ } ], "security": { - "csp": null - } + "csp": "default-src 'self' ipc: http://ipc.localhost; connect-src 'self' ipc: http://ipc.localhost https://hackatime.hackclub.com https://pub-d35fbe65a5b5426bb6d62ff02a8c7d03.r2.dev wss://*.ingest.us.sentry.io https://us.i.posthog.com https://fonts.googleapis.com https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; worker-src 'self' blob:;" + }, + "withGlobalTauri": true }, "bundle": { "active": true, diff --git a/src/App.vue b/src/App.vue index 700d859..9c06717 100644 --- a/src/App.vue +++ b/src/App.vue @@ -28,6 +28,26 @@ if (!(window as any).__hackatimeConsoleWrapped) { console.info('[CONSOLE] Console wrapper initialized - logs will be captured'); } +if (!(window as any).__hackatimeErrorHandlerSet) { + (window as any).__hackatimeErrorHandlerSet = true; + + window.addEventListener('unhandledrejection', (event) => { + console.error('[UNHANDLED REJECTION]', event.reason); + + const errorMessage = event.reason?.message || String(event.reason); + if (errorMessage.includes('callbackId') || + errorMessage.includes('IPC') || + errorMessage.includes('Load failed')) { + event.preventDefault(); + console.warn('[IPC] Suppressed IPC-related error that was already logged'); + } + }); + + window.addEventListener('error', (event) => { + console.error('[UNHANDLED ERROR]', event.error); + }); +} + interface AuthState { is_authenticated: boolean; access_token: string | null; diff --git a/src/main.ts b/src/main.ts index a3e0c8f..bcac316 100644 --- a/src/main.ts +++ b/src/main.ts @@ -38,7 +38,55 @@ Sentry.init({ profilesSampleRate: __SENTRY_ENVIRONMENT__ === 'production' ? 0.1 : 1.0, - enableLogs: true + enableLogs: true, + + beforeSend(event, hint) { + const error = hint.originalException as Error | undefined + const errorMessage = (error as any)?.message || error?.toString() || '' + + if ( + errorMessage.includes('callbackId') || + errorMessage.includes('IPC custom protocol failed') || + errorMessage.includes('Tauri will now use the postMessage interface') || + errorMessage.includes('Load failed') && errorMessage.includes('localhost') || + errorMessage.includes('ipc://localhost') || + errorMessage.includes('project.editors.some') || + errorMessage.includes('project.total_heartbeats.toLocaleString') || + errorMessage.includes('el.__vnode') || + errorMessage.includes('patchElement') && errorMessage.includes('null') || + event.exception?.values?.some(value => + value.value?.includes('callbackId') || + value.value?.includes('[callbackId, data]') || + value.value?.includes('project.editors.some') || + value.value?.includes('project.total_heartbeats') || + value.value?.includes('el.__vnode') || + (value.value?.includes('patchElement') && value.value?.includes('null')) + ) + ) { + console.log('[SENTRY] Filtered out known benign error:', errorMessage) + return null + } + + return event + }, + + ignoreErrors: [ + 'IPC custom protocol failed', + 'callbackId', + 'Load failed', + 'ipc://localhost', + 'undefined is not an object (evaluating \'[callbackId, data]\')', + 'undefined is not an object (evaluating \'project.editors.some\')', + 'undefined is not an object (evaluating \'project.total_heartbeats.toLocaleString\')', + 'null is not an object (evaluating \'el.__vnode = n2\')', + 'null is not an object (evaluating \'el.__vnode\')', + /IPC custom protocol/, + /callbackId/, + /project\.editors\.some/, + /project\.total_heartbeats/, + /el\.__vnode/, + /patchElement/, + ] }) app.mount('#app') diff --git a/src/views/Projects.vue b/src/views/Projects.vue index 8220137..4905327 100644 --- a/src/views/Projects.vue +++ b/src/views/Projects.vue @@ -128,26 +128,26 @@
-

{{ project.name }}

+

{{ project?.name || 'Unnamed' }}

- {{ (project.total_heartbeats || 0).toLocaleString() }} heartbeats - {{ formatDuration(project.total_seconds || 0) }} - + {{ ((project?.total_heartbeats ?? 0)).toLocaleString() }} heartbeats + {{ formatDuration(project?.total_seconds ?? 0) }} + Active recently
- {{ ((project.total_seconds || 0) / 3600).toFixed(1) }}h + {{ ((project?.total_seconds ?? 0) / 3600).toFixed(1) }}h
@@ -155,28 +155,28 @@
{{ language }} - +{{ (project.languages || []).length - 3 }} more + +{{ (project?.languages || []).length - 3 }} more
- + First: {{ formatDate(project.first_heartbeat) }} - + Last: {{ formatDate(project.last_heartbeat) }}
@@ -225,11 +225,11 @@

- {{ selectedProject.name }} + {{ selectedProject?.name || 'Unnamed' }}

- {{ (selectedProject.total_heartbeats || 0).toLocaleString() }} heartbeats - + {{ ((selectedProject?.total_heartbeats ?? 0)).toLocaleString() }} heartbeats + Active recently
@@ -252,17 +252,17 @@
Total Time
- {{ ((selectedProject.total_seconds || 0) / 3600).toFixed(1) }}h + {{ ((selectedProject?.total_seconds ?? 0) / 3600).toFixed(1) }}h
- {{ formatDuration(selectedProject.total_seconds || 0) }} + {{ formatDuration(selectedProject?.total_seconds ?? 0) }}
Heartbeats
- {{ (selectedProject.total_heartbeats || 0).toLocaleString() }} + {{ ((selectedProject?.total_heartbeats ?? 0)).toLocaleString() }}
Activity events @@ -271,12 +271,12 @@
-
+

Languages

@@ -286,12 +286,12 @@
-
+

Editors

@@ -301,7 +301,7 @@
-
+
{ const allLanguages = computed(() => { const languages = new Set(); allProjects.value.forEach(project => { - if (project.languages && Array.isArray(project.languages)) { - project.languages.forEach(lang => languages.add(lang)); + if (project && project.languages && Array.isArray(project.languages)) { + project.languages.forEach(lang => { + if (lang && typeof lang === 'string') { + languages.add(lang); + } + }); } }); return Array.from(languages).sort(); }); const filteredProjects = computed(() => { - let filtered = [...allProjects.value]; + let filtered = [...allProjects.value].filter(project => project && typeof project === 'object'); if (searchQuery.value.trim()) { const query = searchQuery.value.toLowerCase(); - filtered = filtered.filter(project => - project.name?.toLowerCase().includes(query) || - (project.languages || []).some(lang => lang.toLowerCase().includes(query)) || - (project.editors || []).some(editor => editor.toLowerCase().includes(query)) - ); + filtered = filtered.filter(project => { + if (!project) return false; + + const nameMatch = project.name?.toLowerCase().includes(query); + const languageMatch = Array.isArray(project.languages) && + project.languages.some(lang => lang && typeof lang === 'string' && lang.toLowerCase().includes(query)); + const editorMatch = Array.isArray(project.editors) && + project.editors.some(editor => editor && typeof editor === 'string' && editor.toLowerCase().includes(query)); + + return nameMatch || languageMatch || editorMatch; + }); } if (filterLanguage.value) { filtered = filtered.filter(project => - (project.languages || []).includes(filterLanguage.value) + project && Array.isArray(project.languages) && project.languages.includes(filterLanguage.value) ); } switch (sortBy.value) { case "recent": filtered.sort((a, b) => { - const dateA = a.last_heartbeat ? new Date(a.last_heartbeat).getTime() : 0; - const dateB = b.last_heartbeat ? new Date(b.last_heartbeat).getTime() : 0; + const dateA = a?.last_heartbeat ? new Date(a.last_heartbeat).getTime() : 0; + const dateB = b?.last_heartbeat ? new Date(b.last_heartbeat).getTime() : 0; return dateB - dateA; }); break; case "time": - filtered.sort((a, b) => (b.total_seconds || 0) - (a.total_seconds || 0)); + filtered.sort((a, b) => (b?.total_seconds || 0) - (a?.total_seconds || 0)); break; case "name": - filtered.sort((a, b) => (a.name || '').localeCompare(b.name || '')); + filtered.sort((a, b) => (a?.name || '').localeCompare(b?.name || '')); break; case "heartbeats": - filtered.sort((a, b) => (b.total_heartbeats || 0) - (a.total_heartbeats || 0)); + filtered.sort((a, b) => (b?.total_heartbeats || 0) - (a?.total_heartbeats || 0)); break; } @@ -446,6 +456,21 @@ const hasMoreProjects = computed(() => { return paginatedProjects.value.length < filteredProjects.value.length; }); +function normalizeProject(project: any): Project { + return { + name: project?.name || 'Unnamed Project', + total_seconds: Number(project?.total_seconds) || 0, + total_heartbeats: Number(project?.total_heartbeats) || 0, + languages: Array.isArray(project?.languages) ? project.languages : [], + editors: Array.isArray(project?.editors) ? project.editors : [], + first_heartbeat: project?.first_heartbeat || null, + last_heartbeat: project?.last_heartbeat || null, + repo_url: project?.repo_url || null, + recent_activity_seconds: Number(project?.recent_activity_seconds) || 0, + recent_activity_formatted: project?.recent_activity_formatted || '' + }; +} + async function loadProjects() { isLoading.value = true; error.value = null; @@ -464,7 +489,9 @@ async function loadProjects() { apiConfig: props.apiConfig }) as ProjectsResponse; console.log("Projects loaded:", response); - allProjects.value = response.projects || []; + + const projects = response?.projects || []; + allProjects.value = projects.map(normalizeProject).filter(p => p.name && p.name !== 'Unnamed Project'); } catch (err) { console.error("Failed to load projects:", err); error.value = err instanceof Error ? err.message : String(err);