diff --git a/web/build/index.html b/web/build/index.html index 271c044..b304b9f 100644 --- a/web/build/index.html +++ b/web/build/index.html @@ -4,13 +4,13 @@ - - + + - + - + @@ -18,21 +18,21 @@ - +
+ +{#if values.length > 0} + +{/if} diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 6a4d274..033a5fb 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -144,3 +144,13 @@ export function crashRateColor(rate: number | null): string { if (rate < 5) return "text-warning"; return "text-destructive"; } + +/** + * Map crash rate to a bar fill color class (matches crashRateColor thresholds). + */ +export function crashRateBarClass(rate: number | null): string { + if (rate === null) return ""; + if (rate < 1) return "bg-success"; + if (rate < 5) return "bg-warning"; + return "bg-destructive"; +} diff --git a/web/src/routes/(dashboard)/issues/[issueId]/+page.svelte b/web/src/routes/(dashboard)/issues/[issueId]/+page.svelte index ee58b7a..78285a2 100644 --- a/web/src/routes/(dashboard)/issues/[issueId]/+page.svelte +++ b/web/src/routes/(dashboard)/issues/[issueId]/+page.svelte @@ -9,6 +9,7 @@ import { Skeleton } from '$lib/components/ui/skeleton/index.js'; import EmptyState from '$lib/components/EmptyState.svelte'; import Pagination from '$lib/components/Pagination.svelte'; + import Sparkline from '$lib/components/Sparkline.svelte'; import { Table, TableBody, @@ -34,6 +35,25 @@ let totalEventPages: number = $derived(Math.max(1, Math.ceil(totalEvents / eventsPerPage))); + const BUCKET_DAYS = 14; + let eventBuckets: number[] = $state([]); + let bucketRange = $state(''); + + /** Bucket event timestamps into per-day counts for the last N days. */ + function buildBuckets(sample: StoredEvent[]) { + const start = new Date(); + start.setHours(0, 0, 0, 0); + start.setDate(start.getDate() - (BUCKET_DAYS - 1)); + const counts = new Array(BUCKET_DAYS).fill(0); + for (const e of sample) { + const idx = Math.floor((new Date(e.received_at).getTime() - start.getTime()) / 86400000); + if (idx >= 0 && idx < BUCKET_DAYS) counts[idx] += 1; + } + eventBuckets = counts; + const fmt = (d: Date) => d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + bucketRange = `${fmt(start)} - ${fmt(new Date())}`; + } + /** Extract the first exception type/value from a Sentry-format event payload. */ function exceptionSummary(event: StoredEvent): { type: string; value: string } | null { const ex = event.data?.exception as { values?: Array<{ type?: string; value?: string }> } | undefined; @@ -107,6 +127,9 @@ try { issue = await api.getIssue(issueId); await loadEvents(1); + // Non-critical: sample up to 100 events for the daily trend chart. + const sample = await api.listEvents(issueId, 1, 100).catch(() => null); + if (sample) buildBuckets(sample.data); } catch (e: any) { error = e?.message || 'Failed to load issue'; } finally { @@ -225,6 +248,19 @@
+ + {#if eventBuckets.length > 0} + + +
+

Events, last {BUCKET_DAYS} days

+ {bucketRange} +
+ +
+
+ {/if} +
diff --git a/web/src/routes/(dashboard)/release-health/+page.svelte b/web/src/routes/(dashboard)/release-health/+page.svelte index 1091453..4866dd0 100644 --- a/web/src/routes/(dashboard)/release-health/+page.svelte +++ b/web/src/routes/(dashboard)/release-health/+page.svelte @@ -17,7 +17,7 @@ TableHeader, TableRow } from '$lib/components/ui/table/index.js'; - import { timeAgo, crashRateColor } from '$lib/utils'; + import { timeAgo, crashRateColor, crashRateBarClass } from '$lib/utils'; let projects: Project[] = $state([]); let selectedProject: string = $state(''); @@ -291,7 +291,7 @@ {/if} {#if aggregateAbnormal > 0}
{/if} @@ -314,8 +314,8 @@ errored - - {aggregateAbnormal} + + {aggregateAbnormal} abnormal @@ -375,9 +375,19 @@ {/if} - - {s.crash_rate !== null ? s.crash_rate.toFixed(2) + '%' : '—'} - +
+ + {s.crash_rate !== null ? s.crash_rate.toFixed(2) + '%' : '—'} + + {#if s.crash_rate !== null} + + {/if} +