fix: resolve sentry error reports and improve data validation

This commit is contained in:
Leafd 2025-10-09 18:46:56 -04:00
parent ca898884fc
commit 8171e059ae
No known key found for this signature in database
GPG key ID: D44AE7A3699406BE
4 changed files with 140 additions and 44 deletions

View file

@ -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,

View file

@ -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;

View file

@ -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')

View file

@ -128,26 +128,26 @@
<div class="flex-1 overflow-y-auto min-h-0" ref="scrollContainer">
<div class="grid gap-4 pt-2 pb-4">
<div
v-for="project in paginatedProjects"
:key="project.name"
v-for="(project, index) in paginatedProjects"
:key="`${project?.name || 'unnamed'}-${index}`"
class="card-3d"
@click="selectProject(project)"
>
<div class="rounded-[8px] border border-black p-4 card-3d-front cursor-pointer hover:bg-[#4a3a4b] transition-colors" style="background-color: #3D2C3E;">
<div class="flex justify-between items-start mb-3">
<div class="flex-1 min-w-0">
<h4 class="text-white font-semibold text-lg mb-1 truncate" style="font-family: 'Outfit', sans-serif;">{{ project.name }}</h4>
<h4 class="text-white font-semibold text-lg mb-1 truncate" style="font-family: 'Outfit', sans-serif;">{{ project?.name || 'Unnamed' }}</h4>
<div class="flex items-center gap-4 text-sm text-white/60 flex-wrap" style="font-family: 'Outfit', sans-serif;">
<span>{{ (project.total_heartbeats || 0).toLocaleString() }} heartbeats</span>
<span>{{ formatDuration(project.total_seconds || 0) }}</span>
<span v-if="project.recent_activity_seconds && project.recent_activity_seconds > 0" class="text-[#E99682] font-medium">
<span>{{ ((project?.total_heartbeats ?? 0)).toLocaleString() }} heartbeats</span>
<span>{{ formatDuration(project?.total_seconds ?? 0) }}</span>
<span v-if="project?.recent_activity_seconds && project.recent_activity_seconds > 0" class="text-[#E99682] font-medium">
Active recently
</span>
</div>
</div>
<div class="text-right flex-shrink-0">
<div class="text-xl font-bold text-[#E99682]" style="font-family: 'Outfit', sans-serif;">
{{ ((project.total_seconds || 0) / 3600).toFixed(1) }}h
{{ ((project?.total_seconds ?? 0) / 3600).toFixed(1) }}h
</div>
</div>
</div>
@ -155,28 +155,28 @@
<!-- Languages and Editors -->
<div class="flex flex-wrap gap-2 mb-3">
<span
v-for="language in (project.languages || []).slice(0, 3)"
:key="language"
v-for="(language, langIndex) in (project?.languages || []).slice(0, 3)"
:key="`${project?.name}-lang-${langIndex}-${language}`"
class="px-2 py-1 bg-[rgba(233,150,130,0.15)] text-[#E99682] text-xs rounded-md font-medium"
style="font-family: 'Outfit', sans-serif;"
>
{{ language }}
</span>
<span
v-if="(project.languages || []).length > 3"
v-if="(project?.languages || []).length > 3"
class="px-2 py-1 bg-[rgba(50,36,51,0.15)] text-white/60 text-xs rounded-md"
style="font-family: 'Outfit', sans-serif;"
>
+{{ (project.languages || []).length - 3 }} more
+{{ (project?.languages || []).length - 3 }} more
</span>
</div>
<!-- Time Range -->
<div class="text-xs text-white/50" style="font-family: 'Outfit', sans-serif;">
<span v-if="project.first_heartbeat">
<span v-if="project?.first_heartbeat">
First: {{ formatDate(project.first_heartbeat) }}
</span>
<span v-if="project.last_heartbeat" class="ml-4">
<span v-if="project?.last_heartbeat" class="ml-4">
Last: {{ formatDate(project.last_heartbeat) }}
</span>
</div>
@ -225,11 +225,11 @@
<div class="flex items-start justify-between mb-3">
<div class="flex-1 min-w-0">
<h2 class="text-3xl font-bold text-white m-0 mb-2 truncate" style="font-family: 'Outfit', sans-serif;">
{{ selectedProject.name }}
{{ selectedProject?.name || 'Unnamed' }}
</h2>
<div class="flex items-center gap-4 text-white/60 flex-wrap" style="font-family: 'Outfit', sans-serif;">
<span class="text-base">{{ (selectedProject.total_heartbeats || 0).toLocaleString() }} heartbeats</span>
<span v-if="selectedProject.recent_activity_seconds && selectedProject.recent_activity_seconds > 0" class="px-2 py-1 bg-[rgba(233,150,130,0.2)] text-[#E99682] text-sm rounded-md font-medium">
<span class="text-base">{{ ((selectedProject?.total_heartbeats ?? 0)).toLocaleString() }} heartbeats</span>
<span v-if="selectedProject?.recent_activity_seconds && selectedProject.recent_activity_seconds > 0" class="px-2 py-1 bg-[rgba(233,150,130,0.2)] text-[#E99682] text-sm rounded-md font-medium">
Active recently
</span>
</div>
@ -252,17 +252,17 @@
<div class="bg-[rgba(42,31,43,0.5)] border-2 border-[rgba(0,0,0,0.3)] rounded-lg p-4">
<div class="text-white/60 text-sm mb-1" style="font-family: 'Outfit', sans-serif;">Total Time</div>
<div class="text-3xl font-bold text-white" style="font-family: 'Outfit', sans-serif;">
{{ ((selectedProject.total_seconds || 0) / 3600).toFixed(1) }}h
{{ ((selectedProject?.total_seconds ?? 0) / 3600).toFixed(1) }}h
</div>
<div class="text-white/40 text-xs mt-1" style="font-family: 'Outfit', sans-serif;">
{{ formatDuration(selectedProject.total_seconds || 0) }}
{{ formatDuration(selectedProject?.total_seconds ?? 0) }}
</div>
</div>
<div class="bg-[rgba(42,31,43,0.5)] border-2 border-[rgba(0,0,0,0.3)] rounded-lg p-4">
<div class="text-white/60 text-sm mb-1" style="font-family: 'Outfit', sans-serif;">Heartbeats</div>
<div class="text-3xl font-bold text-white" style="font-family: 'Outfit', sans-serif;">
{{ (selectedProject.total_heartbeats || 0).toLocaleString() }}
{{ ((selectedProject?.total_heartbeats ?? 0)).toLocaleString() }}
</div>
<div class="text-white/40 text-xs mt-1" style="font-family: 'Outfit', sans-serif;">
Activity events
@ -271,12 +271,12 @@
</div>
<!-- Languages Section -->
<div v-if="selectedProject.languages && selectedProject.languages.length > 0">
<div v-if="selectedProject?.languages && selectedProject.languages.length > 0">
<h3 class="text-white text-lg font-bold mb-3" style="font-family: 'Outfit', sans-serif;">Languages</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="language in selectedProject.languages"
:key="language"
v-for="(language, langIndex) in selectedProject.languages"
:key="`modal-lang-${langIndex}-${language}`"
class="px-3 py-2 bg-[rgba(233,150,130,0.15)] text-[#E99682] text-sm rounded-lg font-medium border-2 border-[rgba(233,150,130,0.3)]"
style="font-family: 'Outfit', sans-serif;"
>
@ -286,12 +286,12 @@
</div>
<!-- Editors Section -->
<div v-if="selectedProject.editors && selectedProject.editors.length > 0">
<div v-if="selectedProject?.editors && selectedProject.editors.length > 0">
<h3 class="text-white text-lg font-bold mb-3" style="font-family: 'Outfit', sans-serif;">Editors</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="editor in selectedProject.editors"
:key="editor"
v-for="(editor, editorIndex) in selectedProject.editors"
:key="`modal-editor-${editorIndex}-${editor}`"
class="px-3 py-2 bg-[rgba(232,133,146,0.15)] text-[#E88592] text-sm rounded-lg font-medium border-2 border-[rgba(232,133,146,0.3)]"
style="font-family: 'Outfit', sans-serif;"
>
@ -301,7 +301,7 @@
</div>
<!-- Repository Link -->
<div v-if="selectedProject.repo_url">
<div v-if="selectedProject?.repo_url">
<a
:href="selectedProject.repo_url"
target="_blank"
@ -390,47 +390,57 @@ const sortByLabel = computed(() => {
const allLanguages = computed(() => {
const languages = new Set<string>();
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);