diff --git a/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.test.tsx b/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.test.tsx
index 3b0f97c5..54247880 100644
--- a/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.test.tsx
+++ b/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen } from "@testing-library/react";
+import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import Attachment from "./Attachment";
@@ -199,6 +199,30 @@ describe("Attachment", () => {
expect(container.textContent).toContain("Expand to preview attachment");
});
+
+ it("should toggle between expanded and collapsed when the prompt is clicked", () => {
+ const { container } = render(
+ ,
+ );
+
+ // Collapsed by default: shows the prompt, no content decoded yet.
+ expect(container.textContent).toContain("Expand to preview attachment");
+ expect(container.textContent).not.toContain("Hello, World!");
+ expect(mockCreateObjectURL).not.toHaveBeenCalled();
+
+ // Expanding renders the content and builds the download URL on demand.
+ fireEvent.click(screen.getByRole("button", { name: /expand to preview attachment/i }));
+ expect(container.textContent).toContain("Hello, World!");
+ expect(mockCreateObjectURL).toHaveBeenCalledTimes(1);
+
+ // Collapsing hides the content again.
+ fireEvent.click(screen.getByRole("button", { name: /collapse attachment/i }));
+ expect(container.textContent).toContain("Expand to preview attachment");
+ expect(container.textContent).not.toContain("Hello, World!");
+ });
});
describe("cleanup", () => {
diff --git a/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.tsx b/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.tsx
index 9f7b458f..83132d3f 100644
--- a/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.tsx
+++ b/packages/spotlight/src/ui/telemetry/components/insights/envelopes/Attachment.tsx
@@ -1,7 +1,7 @@
import type { EnvelopeItem } from "@sentry/core";
import { ReactComponent as Download } from "@spotlight/ui/assets/download.svg";
import { base64Decode, safeAtob } from "@spotlight/ui/lib/base64";
-import { type ReactNode, useEffect, useMemo } from "react";
+import { type ReactNode, useEffect, useMemo, useState } from "react";
import JsonViewer from "../../shared/JsonViewer";
import { CodeViewer } from "./CodeViewer";
import { inferExtension } from "./contentType";
@@ -23,10 +23,15 @@ export default function Attachment({
const extension = inferExtension(header.content_type as string | null, header.type as string | null);
const name = (header.filename as string) || `untitled.${extension}`;
+ // Lazily render the attachment: start from the `expanded` prop, but let the
+ // user expand it on demand so previews/downloads work wherever the component
+ // is used, without eagerly decoding every attachment.
+ const [isExpanded, setIsExpanded] = useState(expanded);
+
// Create download URL for attachment
// Returns: string (success), null (decode error), undefined (not expanded yet)
const downloadUrl = useMemo(() => {
- if (!expanded) {
+ if (!isExpanded) {
return undefined; // Not needed yet
}
@@ -54,7 +59,7 @@ export default function Attachment({
const blob = new Blob([blobData], { type: contentType || "application/octet-stream" });
return URL.createObjectURL(blob);
- }, [expanded, attachment, extension, header.content_type]);
+ }, [isExpanded, attachment, extension, header.content_type]);
// Cleanup blob URL on unmount or when URL changes
useEffect(() => {
@@ -69,7 +74,7 @@ export default function Attachment({
let content: ReactNode = null;
- if (expanded) {
+ if (isExpanded) {
if (decodeError) {
content = (
@@ -114,7 +119,17 @@ export default function Attachment({
- {expanded ? content : Expand to preview attachment.
}
+
+
+ {isExpanded ? content : null}
+
>
);
}