Skip to content

Commit c6a5c95

Browse files
edde746claude
andcommitted
Display threads, mechanism data, and native addresses in event UI
Add ThreadsDisplay component showing thread list with crashed/current tags and per-thread stacktraces. Show mechanism signal data (fault address, signal number) on exceptions. Fall back to instruction_addr and package for native stack frames without source context. Include all new fields in the copy-as-markdown output. Also extract getFrameName/getFrameLocation utilities to eliminate duplicated frame formatting logic, and make CopyButton accept a lazy text getter to avoid recomputing markdown on every reactive update. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a600804 commit c6a5c95

8 files changed

Lines changed: 171 additions & 23 deletions

File tree

frontend/src/components/events/ExceptionDisplay.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface ExceptionValue {
1010
type?: string;
1111
handled?: boolean;
1212
description?: string;
13+
data?: Record<string, unknown>;
1314
};
1415
stacktrace?: {
1516
frames?: StackFrame[];
@@ -39,6 +40,12 @@ export default function ExceptionDisplay(props: ExceptionDisplayProps) {
3940
{exception.mechanism!.handled === false && (
4041
<span class="exception__unhandled">(unhandled)</span>
4142
)}
43+
<Show when={exception.mechanism!.data}>
44+
{" — "}
45+
{Object.entries(exception.mechanism!.data!)
46+
.map(([k, v]) => `${k}: ${v}`)
47+
.join(", ")}
48+
</Show>
4249
</p>
4350
</Show>
4451
</div>

frontend/src/components/events/StacktraceViewer.tsx

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,26 @@ export interface StackFrame {
1414
pre_context?: string[];
1515
context_line?: string;
1616
post_context?: string[];
17+
instruction_addr?: string;
18+
symbol_addr?: string;
19+
image_addr?: string;
20+
package?: string;
21+
}
22+
23+
export function getFrameName(frame: StackFrame): string {
24+
return frame.function ?? frame.instruction_addr ?? "<anonymous>";
25+
}
26+
27+
export function getFrameLocation(frame: StackFrame): string {
28+
if (frame.filename || frame.abs_path || frame.module) {
29+
let loc = frame.filename ?? frame.abs_path ?? frame.module ?? "";
30+
if (frame.lineno != null) {
31+
loc += `:${frame.lineno}`;
32+
if (frame.colno != null) loc += `:${frame.colno}`;
33+
}
34+
return loc;
35+
}
36+
return frame.package ?? "unknown";
1737
}
1838

1939
interface StacktraceViewerProps {
@@ -69,20 +89,18 @@ export default function StacktraceViewer(props: StacktraceViewerProps) {
6989
!!frame.context_line ||
7090
(frame.pre_context && frame.pre_context.length > 0) ||
7191
(frame.post_context && frame.post_context.length > 0);
92+
const frameName = () => getFrameName(frame);
93+
const frameLocation = () => getFrameLocation(frame);
7294

7395
return (
7496
<div class="stacktrace__frame" data-in-app={frame.in_app ?? false}>
7597
<Show when={hasContext()} fallback={
7698
<div class="stacktrace__frame-btn stacktrace__frame-btn--static">
7799
<span class="stacktrace__fn-name">
78-
{frame.function ?? "<anonymous>"}
100+
{frameName()}
79101
</span>
80102
<span class="stacktrace__file-name">
81-
{frame.filename ?? frame.abs_path ?? frame.module ?? "unknown"}
82-
<Show when={frame.lineno}>
83-
:{frame.lineno}
84-
<Show when={frame.colno}>:{frame.colno}</Show>
85-
</Show>
103+
{frameLocation()}
86104
</span>
87105
<Show when={frame.in_app}>
88106
<span class="stacktrace__app-tag">app</span>
@@ -97,14 +115,10 @@ export default function StacktraceViewer(props: StacktraceViewerProps) {
97115
{isExpanded() ? <IconChevronDown /> : <IconChevronRight />}
98116
</span>
99117
<span class="stacktrace__fn-name">
100-
{frame.function ?? "<anonymous>"}
118+
{frameName()}
101119
</span>
102120
<span class="stacktrace__file-name">
103-
{frame.filename ?? frame.abs_path ?? frame.module ?? "unknown"}
104-
<Show when={frame.lineno}>
105-
:{frame.lineno}
106-
<Show when={frame.colno}>:{frame.colno}</Show>
107-
</Show>
121+
{frameLocation()}
108122
</span>
109123
<Show when={frame.in_app}>
110124
<span class="stacktrace__app-tag">app</span>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { For, Show } from "solid-js";
2+
import StacktraceViewer from "./StacktraceViewer";
3+
import type { StackFrame } from "./StacktraceViewer";
4+
5+
export interface ThreadValue {
6+
id?: unknown;
7+
name?: string;
8+
crashed?: boolean;
9+
current?: boolean;
10+
stacktrace?: {
11+
frames?: StackFrame[];
12+
};
13+
}
14+
15+
interface ThreadsDisplayProps {
16+
threads: ThreadValue[];
17+
}
18+
19+
export default function ThreadsDisplay(props: ThreadsDisplayProps) {
20+
return (
21+
<div>
22+
<For each={props.threads}>
23+
{(thread) => (
24+
<div class="thread" data-crashed={thread.crashed ?? false}>
25+
<div class="thread__header">
26+
<span class="thread__name">
27+
Thread {thread.id != null ? `#${thread.id}` : ""}{thread.name ? ` — ${thread.name}` : ""}
28+
</span>
29+
<Show when={thread.crashed}>
30+
<span class="thread__crashed-tag">crashed</span>
31+
</Show>
32+
<Show when={thread.current}>
33+
<span class="thread__current-tag">current</span>
34+
</Show>
35+
</div>
36+
<Show when={thread.stacktrace?.frames && thread.stacktrace!.frames!.length > 0}>
37+
<StacktraceViewer frames={thread.stacktrace!.frames!} />
38+
</Show>
39+
</div>
40+
)}
41+
</For>
42+
</div>
43+
);
44+
}

frontend/src/components/ui/CopyButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useClipboard } from "~/hooks/useClipboard";
55
import Button from "~/components/ui/Button";
66

77
interface CopyButtonProps {
8-
text: string;
8+
text: string | (() => string);
99
label?: string;
1010
class?: string;
1111
}
@@ -18,7 +18,7 @@ export default function CopyButton(props: CopyButtonProps) {
1818
variant="ghost"
1919
size="sm"
2020
class={props.class}
21-
onClick={() => copy(props.text)}
21+
onClick={() => copy(typeof props.text === "function" ? props.text() : props.text)}
2222
>
2323
<Show when={copied()} fallback={<><IconClipboard /> {props.label ?? "Copy"}</>}>
2424
<IconCheck /> Copied!

frontend/src/pages/DirectEventDetail.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ExceptionDisplay from "~/components/events/ExceptionDisplay";
1111
import BreadcrumbsTimeline from "~/components/events/BreadcrumbsTimeline";
1212
import ContextPanels from "~/components/events/ContextPanels";
1313
import TagsTable from "~/components/events/TagsTable";
14+
import ThreadsDisplay from "~/components/events/ThreadsDisplay";
1415
import IconEye from "~icons/lucide/eye";
1516
import IconEyeOff from "~icons/lucide/eye-off";
1617

@@ -64,6 +65,11 @@ export default function DirectEventDetail() {
6465
return data?.user ?? null;
6566
};
6667

68+
const threads = () => {
69+
const data = parsedData();
70+
return data?.threads?.values ?? [];
71+
};
72+
6773
const tags = () => {
6874
const data = parsedData();
6975
if (!data?.tags) return [];
@@ -110,6 +116,10 @@ export default function DirectEventDetail() {
110116
<ExceptionDisplay exceptions={exceptions()} />
111117
</Show>
112118

119+
<Show when={threads().length > 0}>
120+
<ThreadsDisplay threads={threads()} />
121+
</Show>
122+
113123
<Show when={breadcrumbs().length > 0}>
114124
<BreadcrumbsTimeline breadcrumbs={breadcrumbs()} />
115125
</Show>

frontend/src/pages/EventDetail.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ExceptionDisplay from "~/components/events/ExceptionDisplay";
1111
import BreadcrumbsTimeline from "~/components/events/BreadcrumbsTimeline";
1212
import ContextPanels from "~/components/events/ContextPanels";
1313
import TagsTable from "~/components/events/TagsTable";
14+
import ThreadsDisplay from "~/components/events/ThreadsDisplay";
1415
import IconArrowLeft from "~icons/lucide/arrow-left";
1516
import IconEye from "~icons/lucide/eye";
1617
import IconEyeOff from "~icons/lucide/eye-off";
@@ -69,6 +70,11 @@ export default function EventDetail() {
6970
return data?.user ?? null;
7071
};
7172

73+
const threads = () => {
74+
const data = parsedData();
75+
return data?.threads?.values ?? [];
76+
};
77+
7278
const tags = () => {
7379
const data = parsedData();
7480
if (!data?.tags) return [];
@@ -119,6 +125,10 @@ export default function EventDetail() {
119125
<ExceptionDisplay exceptions={exceptions()} />
120126
</Show>
121127

128+
<Show when={threads().length > 0}>
129+
<ThreadsDisplay threads={threads()} />
130+
</Show>
131+
122132
<Show when={breadcrumbs().length > 0}>
123133
<BreadcrumbsTimeline breadcrumbs={breadcrumbs()} />
124134
</Show>

frontend/src/pages/IssueDetail.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import IconArrowLeft from "~icons/lucide/arrow-left";
1818
import IconArrowRight from "~icons/lucide/arrow-right";
1919
import IconEye from "~icons/lucide/eye";
2020
import IconEyeOff from "~icons/lucide/eye-off";
21+
import { getFrameName, getFrameLocation } from "~/components/events/StacktraceViewer";
2122
import type { ExceptionValue } from "~/components/events/ExceptionDisplay";
2223
import type { Breadcrumb } from "~/components/events/BreadcrumbsTimeline";
2324

@@ -207,8 +208,12 @@ export default function IssueDetail() {
207208
}
208209
if (exc.mechanism) {
209210
const handled = exc.mechanism.handled === false ? " (unhandled)" : "";
211+
let mechLine = `- **Mechanism:** ${exc.mechanism.type ?? "generic"}${handled}`;
212+
if (exc.mechanism.data && Object.keys(exc.mechanism.data).length > 0) {
213+
mechLine += ` — ${Object.entries(exc.mechanism.data).map(([k, v]) => `${k}: ${v}`).join(", ")}`;
214+
}
210215
parts.push("");
211-
parts.push(`- **Mechanism:** ${exc.mechanism.type ?? "generic"}${handled}`);
216+
parts.push(mechLine);
212217
}
213218
const frames = exc.stacktrace?.frames;
214219
if (frames && frames.length > 0) {
@@ -220,14 +225,33 @@ export default function IssueDetail() {
220225
const reversed = [...frames].reverse();
221226
for (const frame of reversed) {
222227
const tag = frame.in_app ? "app" : "";
223-
const fn = frame.function ?? "<anonymous>";
224-
const file = frame.filename ?? frame.abs_path ?? frame.module ?? "unknown";
225-
let loc = file;
226-
if (frame.lineno != null) {
227-
loc += `:${frame.lineno}`;
228-
if (frame.colno != null) loc += `:${frame.colno}`;
228+
parts.push(`| ${tag} | ${getFrameName(frame)} | ${getFrameLocation(frame)} |`);
229+
}
230+
}
231+
}
232+
233+
// Threads
234+
const data = parsedData();
235+
const threadValues = data?.threads?.values as Array<{ id?: unknown; name?: string; crashed?: boolean; current?: boolean; stacktrace?: { frames?: Array<Record<string, unknown>> } }> | undefined;
236+
if (threadValues && threadValues.length > 0) {
237+
parts.push("");
238+
parts.push("## Threads");
239+
for (const thread of threadValues) {
240+
const label = thread.id != null ? `Thread #${thread.id}` : "Thread";
241+
const name = thread.name ? ` — ${thread.name}` : "";
242+
const tags = [thread.crashed ? "crashed" : "", thread.current ? "current" : ""].filter(Boolean).join(", ");
243+
parts.push("");
244+
parts.push(`### ${label}${name}${tags ? ` (${tags})` : ""}`);
245+
const frames = thread.stacktrace?.frames;
246+
if (frames && frames.length > 0) {
247+
parts.push("");
248+
parts.push("| | Function | File |");
249+
parts.push("|---|---|---|");
250+
const reversed = [...frames].reverse();
251+
for (const frame of reversed) {
252+
const tag = frame.in_app ? "app" : "";
253+
parts.push(`| ${tag} | ${getFrameName(frame as any)} | ${getFrameLocation(frame as any)} |`);
229254
}
230-
parts.push(`| ${tag} | ${fn} | ${loc} |`);
231255
}
232256
}
233257
}
@@ -367,7 +391,7 @@ export default function IssueDetail() {
367391
)}
368392
</div>
369393
<div class="inline-gap">
370-
<CopyButton text={buildMarkdown()} label="Copy as Markdown" />
394+
<CopyButton text={buildMarkdown} label="Copy as Markdown" />
371395
<Show when={issue().status !== "resolved"}>
372396
<Button
373397
variant="secondary"

frontend/src/styles/globals.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,6 +1262,45 @@ label.field-label--sm {
12621262
color: var(--color-level-error-text);
12631263
}
12641264

1265+
/* ===================================================================
1266+
THREADS
1267+
=================================================================== */
1268+
1269+
.thread {
1270+
margin-bottom: 16px;
1271+
}
1272+
1273+
.thread__header {
1274+
display: flex;
1275+
align-items: center;
1276+
gap: 8px;
1277+
margin-bottom: 8px;
1278+
}
1279+
1280+
.thread__name {
1281+
font-size: 13px;
1282+
font-weight: 600;
1283+
color: var(--color-text-primary);
1284+
}
1285+
1286+
.thread__crashed-tag {
1287+
font-size: 11px;
1288+
font-weight: 600;
1289+
padding: 1px 6px;
1290+
border-radius: 4px;
1291+
background: var(--color-level-error-bg);
1292+
color: var(--color-level-error-text);
1293+
}
1294+
1295+
.thread__current-tag {
1296+
font-size: 11px;
1297+
font-weight: 600;
1298+
padding: 1px 6px;
1299+
border-radius: 4px;
1300+
background: var(--color-surface-2);
1301+
color: var(--color-text-secondary);
1302+
}
1303+
12651304
/* ===================================================================
12661305
RAW JSON PANEL
12671306
=================================================================== */

0 commit comments

Comments
 (0)