Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions web/build/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,35 @@
<meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/_app/immutable/entry/start.BiqjHQnY.js" rel="modulepreload">
<link href="/_app/immutable/chunks/DLVqjwSU.js" rel="modulepreload">
<link href="/_app/immutable/entry/start.DloRK_ld.js" rel="modulepreload">
<link href="/_app/immutable/chunks/DhdZQ6Fn.js" rel="modulepreload">
<link href="/_app/immutable/chunks/DtfuHTUv.js" rel="modulepreload">
<link href="/_app/immutable/entry/app.DvuAAeOH.js" rel="modulepreload">
<link href="/_app/immutable/entry/app.DcaH8u5r.js" rel="modulepreload">
<link href="/_app/immutable/chunks/kNaey6uv.js" rel="modulepreload">
<link href="/_app/immutable/chunks/xihTtKlq.js" rel="modulepreload">
<link href="/_app/immutable/nodes/0.Dya_MPtI.js" rel="modulepreload">
<link href="/_app/immutable/nodes/0.Cl2pHMx4.js" rel="modulepreload">
<link href="/_app/immutable/chunks/DKyKpwK7.js" rel="modulepreload">
<link href="/_app/immutable/chunks/5Qa-t89S.js" rel="modulepreload">
<link href="/_app/immutable/chunks/hKaPXkrQ.js" rel="modulepreload">
<link href="/_app/immutable/chunks/BscTVQ9J.js" rel="modulepreload">
<link href="/_app/immutable/chunks/B0RXJPsG.js" rel="modulepreload">
<link href="/_app/immutable/chunks/fe7zj1-L.js" rel="modulepreload">

<link href="/_app/immutable/assets/0.DWoDaaiW.css" rel="stylesheet">
<link href="/_app/immutable/assets/0.CqeqEt7D.css" rel="stylesheet">
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">
<script>
{
__sveltekit_mtser5 = {
__sveltekit_vs9042 = {
base: ""
};

const element = document.currentScript.parentElement;

Promise.all([
import("/_app/immutable/entry/start.BiqjHQnY.js"),
import("/_app/immutable/entry/app.DvuAAeOH.js")
import("/_app/immutable/entry/start.DloRK_ld.js"),
import("/_app/immutable/entry/app.DcaH8u5r.js")
]).then(([kit, app]) => {
kit.start(app, element);
});
Expand Down
3 changes: 3 additions & 0 deletions web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
--destructive-foreground: oklch(0.985 0 0);
--success: oklch(0.53 0.15 150);
--warning: oklch(0.62 0.15 75);
--abnormal: oklch(0.58 0.15 55);
--info: oklch(0.53 0.13 240);
--border: oklch(0.9 0.005 265);
--input: oklch(0.9 0.005 265);
Expand All @@ -52,6 +53,7 @@
--destructive-foreground: oklch(0.985 0 0);
--success: oklch(0.74 0.16 150);
--warning: oklch(0.8 0.15 85);
--abnormal: oklch(0.75 0.14 55);
--info: oklch(0.72 0.12 240);
--border: oklch(0.3 0.015 265);
--input: oklch(0.3 0.015 265);
Expand All @@ -78,6 +80,7 @@
--color-destructive-foreground: var(--destructive-foreground);
--color-success: var(--success);
--color-warning: var(--warning);
--color-abnormal: var(--abnormal);
--color-info: var(--info);
--color-border: var(--border);
--color-input: var(--input);
Expand Down
28 changes: 28 additions & 0 deletions web/src/lib/components/Sparkline.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
interface Props {
/** Buckets to plot; zero values still render as a visible empty slot. */
values: number[];
/** Accessible description; screen readers read this instead of the bars. */
label: string;
}

let { values, label }: Props = $props();

let max = $derived(Math.max(...values, 0));

function barPct(v: number): number {
if (max <= 0 || v <= 0) return 2;
return Math.max(3, (v / max) * 100);
}
</script>

{#if values.length > 0}
<div class="flex items-end gap-0.5 h-12 w-full" role="img" aria-label={label}>
{#each values as v, i (i)}
<div
class="flex-1 rounded-[2px] {v === 0 ? 'bg-muted-foreground/25' : 'bg-primary'}"
style="height: {barPct(v)}%"
></div>
{/each}
</div>
{/if}
10 changes: 10 additions & 0 deletions web/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
36 changes: 36 additions & 0 deletions web/src/routes/(dashboard)/issues/[issueId]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<number>(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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -225,6 +248,19 @@
</Card>
</div>

<!-- Events per day -->
{#if eventBuckets.length > 0}
<Card>
<CardContent class="p-4 space-y-2">
<div class="flex items-center justify-between">
<p class="text-xs font-medium text-muted-foreground">Events, last {BUCKET_DAYS} days</p>
<span class="text-xs text-muted-foreground tabular-nums">{bucketRange}</span>
</div>
<Sparkline values={eventBuckets} label="Events per day over the last {BUCKET_DAYS} days" />
</CardContent>
</Card>
{/if}

<!-- Events -->
<div class="space-y-3">
<div class="flex items-center justify-between">
Expand Down
26 changes: 18 additions & 8 deletions web/src/routes/(dashboard)/release-health/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand Down Expand Up @@ -291,7 +291,7 @@
{/if}
{#if aggregateAbnormal > 0}
<div
class="bg-orange-500 h-full"
class="bg-abnormal h-full"
style="width: {(aggregateAbnormal / aggregateTotal * 100)}%"
></div>
{/if}
Expand All @@ -314,8 +314,8 @@
<span class="text-muted-foreground">errored</span>
</span>
<span class="flex items-center gap-1.5">
<span class="h-2.5 w-2.5 rounded-full bg-orange-500"></span>
<span class="text-orange-600 dark:text-orange-400 font-medium">{aggregateAbnormal}</span>
<span class="h-2.5 w-2.5 rounded-full bg-abnormal"></span>
<span class="text-abnormal font-medium">{aggregateAbnormal}</span>
<span class="text-muted-foreground">abnormal</span>
</span>
<span class="flex items-center gap-1.5">
Expand Down Expand Up @@ -375,17 +375,27 @@
{/if}
</TableCell>
<TableCell>
<span class="font-mono font-medium {crashRateColor(s.crash_rate)}">
{s.crash_rate !== null ? s.crash_rate.toFixed(2) + '%' : '—'}
</span>
<div class="min-w-[88px] space-y-1">
<span class="font-mono font-medium tabular-nums {crashRateColor(s.crash_rate)}">
{s.crash_rate !== null ? s.crash_rate.toFixed(2) + '%' : '—'}
</span>
{#if s.crash_rate !== null}
<span class="block h-1.5 w-full rounded-full overflow-hidden bg-muted" aria-hidden="true">
<span
class="block h-full rounded-full {crashRateBarClass(s.crash_rate)}"
style="width: {Math.min(100, s.crash_rate)}%"
></span>
</span>
{/if}
</div>
</TableCell>
<TableCell class="hidden md:table-cell text-center text-success font-medium">
{s.exited}
</TableCell>
<TableCell class="hidden md:table-cell text-center text-warning font-medium">
{s.errored}
</TableCell>
<TableCell class="hidden md:table-cell text-center text-orange-600 dark:text-orange-400 font-medium">
<TableCell class="hidden md:table-cell text-center text-abnormal font-medium">
{s.abnormal}
</TableCell>
<TableCell class="text-center text-destructive font-medium">
Expand Down
16 changes: 14 additions & 2 deletions web/src/routes/(dashboard)/transactions/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
let totalTransactions: number = $state(0);
const perPage = 20;
let totalPages: number = $derived(Math.max(1, Math.ceil(totalTransactions / perPage)));
let maxDuration: number = $derived(Math.max(...transactions.map((t) => t.duration_ms), 1));
let loading = $state(true);
let error = $state('');

Expand Down Expand Up @@ -191,8 +192,19 @@
onclick={() => goto(`/transactions/${txn.id}?project=${selectedProject}`)}
>
<TableCell class="whitespace-normal font-medium">{txn.name}</TableCell>
<TableCell class="font-mono text-muted-foreground">
{formatDuration(txn.duration_ms)}
<TableCell class="font-mono text-muted-foreground whitespace-nowrap">
<div class="flex items-center gap-2">
<span class="tabular-nums">{formatDuration(txn.duration_ms)}</span>
<span
class="hidden md:block h-1.5 w-20 rounded-full overflow-hidden bg-muted"
aria-hidden="true"
>
<span
class="block h-full rounded-full bg-primary/60"
style="width: {Math.min(100, (txn.duration_ms / maxDuration) * 100)}%"
></span>
</span>
</div>
</TableCell>
<TableCell>
<Badge variant="outline" class={transactionStatusTextClass(txn.status)}>
Expand Down
Loading