From 86a9e75132b3522d1e96aff8aa1cff5bba503dc8 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:30:49 +0000 Subject: [PATCH 1/3] feat(local): Show an error event's attachments in its detail panel SDKs send screenshots, view hierarchies and other files in the error's envelope, but the viewer only listed them in the Attachments view. Error event details now include an Attachments section with every attachment whose envelope header names the event, either by the event's own envelope ID or its payload event_id. Rows reuse the collapsed, lazily decoded attachment list from raw envelopes. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- apps/local/README.md | 3 +- .../src/components/attachment-detail.tsx | 6 +- .../src/components/event-detail.test.tsx | 60 +++++++++++++++++++ apps/local/src/components/event-detail.tsx | 28 +++++++-- apps/local/src/lib/attachments.ts | 21 +++++++ 5 files changed, 110 insertions(+), 8 deletions(-) diff --git a/apps/local/README.md b/apps/local/README.md index acc8d5dca..dee7790fe 100644 --- a/apps/local/README.md +++ b/apps/local/README.md @@ -55,7 +55,8 @@ receiver's envelope retention limit when replaying a large corpus. - Errors include exception causes, captured source lines and local variables, breadcrumbs, request/context data, tags, modules, VS Code links for absolute - local paths, and a copyable Markdown summary for AI assistants. + local paths, a copyable Markdown summary for AI assistants, and the + attachments the SDK sent for the event (collapsed until previewed). - AI inspection reads standard `gen_ai.*` and Vercel AI fields from both embedded transaction spans and standalone spans. Prompts, responses, token usage and tool arguments/results appear when the SDK captured them. Child AI steps stay diff --git a/apps/local/src/components/attachment-detail.tsx b/apps/local/src/components/attachment-detail.tsx index 3e3381b8b..b6e14f10f 100644 --- a/apps/local/src/components/attachment-detail.tsx +++ b/apps/local/src/components/attachment-detail.tsx @@ -129,14 +129,16 @@ export function AttachmentDetail({ ); } -export function EnvelopeAttachments({ +export function AttachmentList({ + label, attachments, }: { + label: string; attachments: readonly LocalFeedItem[]; }) { if (!attachments.length) return null; return ( -
+
{attachments.map((attachment) => ( ))} diff --git a/apps/local/src/components/event-detail.test.tsx b/apps/local/src/components/event-detail.test.tsx index 2cb389db4..901bdab1e 100644 --- a/apps/local/src/components/event-detail.test.tsx +++ b/apps/local/src/components/event-detail.test.tsx @@ -229,3 +229,63 @@ test("a related trace suppresses the empty-details message", () => { expect(screen.getByRole("region", { name: "Trace waterfall" })).toBeDefined(); expect(screen.queryByText("No additional event details.")).toBeNull(); }); + +test("errors list their captured attachments collapsed until previewed", () => { + const eventId = "a".repeat(32); + const attachment = (filename: string, data: string) => [ + { type: "attachment", filename, content_type: "text/plain" }, + { data }, + ]; + // The bundled fixture shows envelope and payload IDs can differ; match either. + const sameEnvelope = decodeEnvelope( + JSON.stringify([ + { event_id: "d".repeat(32) }, + [ + [{ type: "event" }, { event_id: eventId, message: "Checkout failed" }], + attachment("screen.txt", "captured screen"), + ], + ]), + "error", + ); + const [laterAttachment] = decodeEnvelope( + JSON.stringify([ + { event_id: eventId }, + [attachment("hierarchy.txt", "view tree")], + ]), + "later", + ); + const [otherAttachment] = decodeEnvelope( + JSON.stringify([ + { event_id: "b".repeat(32) }, + [attachment("other.txt", "another event")], + ]), + "other", + ); + const event = sameEnvelope[0]!; + render( + , + ); + const attachments = screen.getByRole("region", { name: "Event attachments" }); + expect(attachments.textContent).toContain("screen.txt"); + expect(attachments.textContent).toContain("hierarchy.txt"); + expect(attachments.textContent).not.toContain("other.txt"); + expect(attachments.textContent).not.toContain("captured screen"); + + fireEvent.click( + screen.getByRole("button", { name: "Show screen.txt preview" }), + ); + expect(attachments.textContent).toContain("captured screen"); + expect(attachments.textContent).not.toContain("view tree"); +}); + +test("errors without captured attachments show no attachment section", () => { + const event = item("event", { event_id: "c".repeat(32), message: "Boom" }); + render(); + expect( + screen.queryByRole("region", { name: "Event attachments" }), + ).toBeNull(); + expect(screen.queryByRole("heading", { name: "Attachments" })).toBeNull(); +}); diff --git a/apps/local/src/components/event-detail.tsx b/apps/local/src/components/event-detail.tsx index 6c0566551..60c5d6c2c 100644 --- a/apps/local/src/components/event-detail.tsx +++ b/apps/local/src/components/event-detail.tsx @@ -1,7 +1,7 @@ -import { envelopeAttachments } from "@/lib/attachments.ts"; +import { envelopeAttachments, eventAttachments } from "@/lib/attachments.ts"; import { EventContext, FeedbackInspector } from "./event-context.tsx"; import { AiInspector } from "./ai-inspector.tsx"; -import { AttachmentDetail, EnvelopeAttachments } from "./attachment-detail.tsx"; +import { AttachmentDetail, AttachmentList } from "./attachment-detail.tsx"; import { errorMarkdown, inspectAi, hasEventContext } from "@/lib/inspection.ts"; import { Button } from "./ui/button.tsx"; import { useCopyToClipboard } from "@uidotdev/usehooks"; @@ -107,8 +107,13 @@ export function EventDetail({ const [, copyForAi] = useCopyToClipboard(); const ai = inspectAi(item.payload); const attachments = useMemo( - () => (item.type === "envelope" ? envelopeAttachments(item) : []), - [item], + () => + item.type === "envelope" + ? envelopeAttachments(item) + : item.type === "event" + ? eventAttachments(item, relatedItems ?? []) + : [], + [item, relatedItems], ); const hasContext = (item.type === "event" || item.type === "transaction") && @@ -226,7 +231,10 @@ export function EventDetail({ ) : null} {item.type === "envelope" ? ( - + ) : null} {item.type === "feedback" || item.type === "user_report" ? (
) : null} + {item.type === "event" && attachments.length ? ( +
+

Attachments

+ +
+ ) : null} {!fields.length && !(isRequestOrSpan && summaryFields.length) && !exceptions.length && !attributes.length && + !attachments.length && !ai && !hasContext && !trace && diff --git a/apps/local/src/lib/attachments.ts b/apps/local/src/lib/attachments.ts index 8b25e7738..80dd52451 100644 --- a/apps/local/src/lib/attachments.ts +++ b/apps/local/src/lib/attachments.ts @@ -91,3 +91,24 @@ export function envelopeAttachments(item: LocalFeedItem): LocalFeedItem[] { : [], ); } + +/** + * Attachments captured for an event. SDKs send them in the event's envelope or + * in a later envelope whose header names the event. + */ +export function eventAttachments( + event: LocalFeedItem, + items: readonly LocalFeedItem[], +): LocalFeedItem[] { + const ids = [ + text(event.envelopeHeader?.event_id), + text(record(event.payload).event_id), + ].filter((id): id is string => id !== undefined); + if (!ids.length) return []; + return items.filter((candidate) => { + const id = text(candidate.envelopeHeader?.event_id); + return ( + candidate.type === "attachment" && id !== undefined && ids.includes(id) + ); + }); +} From 352fe723401b18fb8b5f702f300239aacfa4d24c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:36:45 +0000 Subject: [PATCH 2/3] ref(local): Use a Set for event attachment ID matching --- apps/local/src/lib/attachments.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/local/src/lib/attachments.ts b/apps/local/src/lib/attachments.ts index 80dd52451..651b3aa76 100644 --- a/apps/local/src/lib/attachments.ts +++ b/apps/local/src/lib/attachments.ts @@ -100,15 +100,15 @@ export function eventAttachments( event: LocalFeedItem, items: readonly LocalFeedItem[], ): LocalFeedItem[] { - const ids = [ - text(event.envelopeHeader?.event_id), - text(record(event.payload).event_id), - ].filter((id): id is string => id !== undefined); - if (!ids.length) return []; + const ids = new Set( + [ + text(event.envelopeHeader?.event_id), + text(record(event.payload).event_id), + ].filter((id): id is string => id !== undefined), + ); + if (!ids.size) return []; return items.filter((candidate) => { const id = text(candidate.envelopeHeader?.event_id); - return ( - candidate.type === "attachment" && id !== undefined && ids.includes(id) - ); + return candidate.type === "attachment" && id !== undefined && ids.has(id); }); } From 64ab4d804e1e53404e850606e9ba7af1a509f3bd Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:46:06 +0000 Subject: [PATCH 3/3] Revert "ref(local): Use a Set for event attachment ID matching" The ID list holds at most two entries, so Array#includes is simpler and no slower. This reverts commit 352fe723401b18fb8b5f702f300239aacfa4d24c. --- apps/local/src/lib/attachments.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/local/src/lib/attachments.ts b/apps/local/src/lib/attachments.ts index 651b3aa76..80dd52451 100644 --- a/apps/local/src/lib/attachments.ts +++ b/apps/local/src/lib/attachments.ts @@ -100,15 +100,15 @@ export function eventAttachments( event: LocalFeedItem, items: readonly LocalFeedItem[], ): LocalFeedItem[] { - const ids = new Set( - [ - text(event.envelopeHeader?.event_id), - text(record(event.payload).event_id), - ].filter((id): id is string => id !== undefined), - ); - if (!ids.size) return []; + const ids = [ + text(event.envelopeHeader?.event_id), + text(record(event.payload).event_id), + ].filter((id): id is string => id !== undefined); + if (!ids.length) return []; return items.filter((candidate) => { const id = text(candidate.envelopeHeader?.event_id); - return candidate.type === "attachment" && id !== undefined && ids.has(id); + return ( + candidate.type === "attachment" && id !== undefined && ids.includes(id) + ); }); }