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
3 changes: 2 additions & 1 deletion apps/local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions apps/local/src/components/attachment-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,16 @@ export function AttachmentDetail({
</section>
);
}
export function EnvelopeAttachments({
export function AttachmentList({
label,
attachments,
}: {
label: string;
attachments: readonly LocalFeedItem[];
}) {
if (!attachments.length) return null;
return (
<section aria-label="Envelope attachments" className="space-y-4">
<section aria-label={label} className="space-y-4">
{attachments.map((attachment) => (
<AttachmentDetail key={attachment.id} item={attachment} collapsible />
))}
Expand Down
60 changes: 60 additions & 0 deletions apps/local/src/components/event-detail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<EventDetail
item={event}
relatedItems={[...sameEnvelope, laterAttachment!, otherAttachment!]}
/>,
);
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(<EventDetail item={event} relatedItems={[event]} />);
expect(
screen.queryByRole("region", { name: "Event attachments" }),
).toBeNull();
expect(screen.queryByRole("heading", { name: "Attachments" })).toBeNull();
});
28 changes: 23 additions & 5 deletions apps/local/src/components/event-detail.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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") &&
Expand Down Expand Up @@ -226,7 +231,10 @@ export function EventDetail({
<AttachmentDetail item={item} />
) : null}
{item.type === "envelope" ? (
<EnvelopeAttachments attachments={attachments} />
<AttachmentList
label="Envelope attachments"
attachments={attachments}
/>
) : null}
{item.type === "feedback" || item.type === "user_report" ? (
<FeedbackInspector
Expand Down Expand Up @@ -272,10 +280,20 @@ export function EventDetail({
/>
</section>
) : null}
{item.type === "event" && attachments.length ? (
<section className="space-y-2">
<h3 className="text-sm font-medium">Attachments</h3>
<AttachmentList
label="Event attachments"
attachments={attachments}
/>
</section>
) : null}
{!fields.length &&
!(isRequestOrSpan && summaryFields.length) &&
!exceptions.length &&
!attributes.length &&
!attachments.length &&
!ai &&
!hasContext &&
!trace &&
Expand Down
21 changes: 21 additions & 0 deletions apps/local/src/lib/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
});
}
Loading