diff --git a/static/app/components/events/attachmentViewers/previewAttachmentTypes.tsx b/static/app/components/events/attachmentViewers/previewAttachmentTypes.tsx
index 1c9f4250e485a6..4153035c9e8bc0 100644
--- a/static/app/components/events/attachmentViewers/previewAttachmentTypes.tsx
+++ b/static/app/components/events/attachmentViewers/previewAttachmentTypes.tsx
@@ -1,7 +1,7 @@
import ImageViewer from 'sentry/components/events/attachmentViewers/imageViewer';
import JsonViewer from 'sentry/components/events/attachmentViewers/jsonViewer';
import LogFileViewer from 'sentry/components/events/attachmentViewers/logFileViewer';
-import type RRWebJsonViewer from 'sentry/components/events/attachmentViewers/rrwebJsonViewer';
+import RRWebJsonViewer from 'sentry/components/events/attachmentViewers/rrwebJsonViewer';
import {WebMViewer} from 'sentry/components/events/attachmentViewers/webmViewer';
import type {IssueAttachment} from 'sentry/types/group';
@@ -37,26 +37,36 @@ type AttachmentRenderer =
| typeof RRWebJsonViewer
| typeof WebMViewer;
-export const getInlineAttachmentRenderer = (
+export const getImageAttachmentRenderer = (
attachment: IssueAttachment
): AttachmentRenderer | undefined => {
if (imageMimeTypes.includes(attachment.mimetype)) {
return ImageViewer;
}
+ if (webmMimeType === attachment.mimetype) {
+ return WebMViewer;
+ }
+ return undefined;
+};
+
+export const getInlineAttachmentRenderer = (
+ attachment: IssueAttachment
+): AttachmentRenderer | undefined => {
+ const imageAttachmentRenderer = getImageAttachmentRenderer(attachment);
+ if (imageAttachmentRenderer) {
+ return imageAttachmentRenderer;
+ }
if (logFileMimeTypes.includes(attachment.mimetype)) {
return LogFileViewer;
}
- if (
- (jsonMimeTypes.includes(attachment.mimetype) && attachment.name === 'rrweb.json') ||
- attachment.name.startsWith('rrweb-')
- ) {
- return JsonViewer;
- }
+ if (jsonMimeTypes.includes(attachment.mimetype)) {
+ if (attachment.name === 'rrweb.json' || attachment.name.startsWith('rrweb-')) {
+ return RRWebJsonViewer;
+ }
- if (webmMimeType === attachment.mimetype) {
- return WebMViewer;
+ return JsonViewer;
}
return undefined;
diff --git a/static/app/components/events/eventTagsAndScreenshot/screenshot/index.tsx b/static/app/components/events/eventTagsAndScreenshot/screenshot/index.tsx
index 1bf86a1c0a34ec..e55d32472ae74e 100644
--- a/static/app/components/events/eventTagsAndScreenshot/screenshot/index.tsx
+++ b/static/app/components/events/eventTagsAndScreenshot/screenshot/index.tsx
@@ -1,5 +1,5 @@
import type {ReactEventHandler} from 'react';
-import {Fragment, useState} from 'react';
+import {useState} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
@@ -8,8 +8,9 @@ import {openConfirmModal} from 'sentry/components/confirm';
import {Button} from 'sentry/components/core/button';
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
+import ImageViewer from 'sentry/components/events/attachmentViewers/imageViewer';
import {
- getInlineAttachmentRenderer,
+ getImageAttachmentRenderer,
imageMimeTypes,
webmMimeType,
} from 'sentry/components/events/attachmentViewers/previewAttachmentTypes';
@@ -70,115 +71,105 @@ function Screenshot({
onDelete(screenshotAttachmentId);
}
- function renderContent(screenshotAttachment: EventAttachment) {
- const AttachmentComponent = getInlineAttachmentRenderer(screenshotAttachment)!;
-
- const downloadUrl = `/api/0/projects/${organization.slug}/${projectSlug}/events/${eventId}/attachments/${screenshot.id}/`;
-
- return (
-
- {totalScreenshots > 1 && (
-
+ const AttachmentComponent = getImageAttachmentRenderer(screenshot) ?? ImageViewer;
+ const downloadUrl = `/api/0/projects/${organization.slug}/${projectSlug}/events/${eventId}/attachments/${screenshot.id}/`;
+
+ return (
+
+ {totalScreenshots > 1 && (
+
+ }
+ size="xs"
+ />
+ {tct('[currentScreenshot] of [totalScreenshots]', {
+ currentScreenshot: screenshotInFocus + 1,
+ totalScreenshots,
+ })}
+ }
+ size="xs"
+ />
+
+ )}
+ 1}>
+ {loadingImage && (
+
+
+
+ )}
+ openVisualizationModal(screenshot, `${downloadUrl}?download=1`)}
+ >
+ setLoadingImage(false)}
+ onError={() => setLoadingImage(false)}
+ controls={false}
+ onCanPlay={() => setLoadingImage(false)}
+ />
+
+
+ {!onlyRenderScreenshot && (
+
+
}
size="xs"
- />
- {tct('[currentScreenshot] of [totalScreenshots]', {
- currentScreenshot: screenshotInFocus + 1,
- totalScreenshots,
- })}
- }
+ onClick={() =>
+ openVisualizationModal(screenshot, `${downloadUrl}?download=1`)
+ }
+ >
+ {t('View screenshot')}
+
+ ,
+ 'aria-label': t('More screenshot actions'),
+ }}
size="xs"
- />
-
- )}
- 1}>
- {loadingImage && (
-
-
-
- )}
-
- openVisualizationModal(screenshot, `${downloadUrl}?download=1`)
- }
- >
- setLoadingImage(false)}
- onError={() => setLoadingImage(false)}
- controls={false}
- onCanPlay={() => setLoadingImage(false)}
- />
-
-
- {!onlyRenderScreenshot && (
-
-
-
- ,
- 'aria-label': t('More screenshot actions'),
- }}
- size="xs"
- items={[
- {
- key: 'download',
- label: t('Download'),
- onAction: () => {
- window.location.assign(`${downloadUrl}?download=1`);
- trackAnalytics(
- 'issue_details.issue_tab.screenshot_dropdown_download',
- {organization}
- );
- },
- },
- {
- key: 'delete',
- label: t('Delete'),
- onAction: () =>
- openConfirmModal({
- header: t('Delete this image?'),
- message: t(
- 'This image was captured around the time that the event occurred. Are you sure you want to delete this image?'
- ),
- onConfirm: () => handleDelete(screenshotAttachment.id),
- }),
+ items={[
+ {
+ key: 'download',
+ label: t('Download'),
+ onAction: () => {
+ window.location.assign(`${downloadUrl}?download=1`);
+ trackAnalytics(
+ 'issue_details.issue_tab.screenshot_dropdown_download',
+ {organization}
+ );
},
- ]}
- />
-
-
- )}
-
- );
- }
-
- return {renderContent(screenshot)};
+ },
+ {
+ key: 'delete',
+ label: t('Delete'),
+ onAction: () =>
+ openConfirmModal({
+ header: t('Delete this image?'),
+ message: t(
+ 'This image was captured around the time that the event occurred. Are you sure you want to delete this image?'
+ ),
+ onConfirm: () => handleDelete(screenshot.id),
+ }),
+ },
+ ]}
+ />
+
+
+ )}
+
+ );
}
export default Screenshot;
diff --git a/static/app/components/events/eventTagsAndScreenshot/screenshot/modal.tsx b/static/app/components/events/eventTagsAndScreenshot/screenshot/modal.tsx
index 6fb5e28c396b6a..ad5ac7bc5c84eb 100644
--- a/static/app/components/events/eventTagsAndScreenshot/screenshot/modal.tsx
+++ b/static/app/components/events/eventTagsAndScreenshot/screenshot/modal.tsx
@@ -1,8 +1,7 @@
import type {ComponentProps} from 'react';
-import {Fragment, useCallback, useEffect, useMemo, useState} from 'react';
+import {Fragment, useCallback, useMemo, useState} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
-import * as Sentry from '@sentry/react';
import type {ModalRenderProps} from 'sentry/actionCreators/modal';
import Confirm from 'sentry/components/confirm';
@@ -11,7 +10,8 @@ import {ButtonBar} from 'sentry/components/core/button/buttonBar';
import {LinkButton} from 'sentry/components/core/button/linkButton';
import {Flex} from 'sentry/components/core/layout';
import {DateTime} from 'sentry/components/dateTime';
-import {getInlineAttachmentRenderer} from 'sentry/components/events/attachmentViewers/previewAttachmentTypes';
+import ImageViewer from 'sentry/components/events/attachmentViewers/imageViewer';
+import {getImageAttachmentRenderer} from 'sentry/components/events/attachmentViewers/previewAttachmentTypes';
import {KeyValueData} from 'sentry/components/keyValueData';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
@@ -106,18 +106,8 @@ export default function ScreenshotModal({
};
}
- const AttachmentComponent = getInlineAttachmentRenderer(currentEventAttachment)!;
-
- useEffect(() => {
- if (currentEventAttachment && !AttachmentComponent) {
- Sentry.withScope(scope => {
- scope.setExtra('mimetype', currentEventAttachment.mimetype);
- scope.setExtra('attachmentName', currentEventAttachment.name);
- scope.setFingerprint(['no-inline-attachment-renderer']);
- scope.captureException(new Error('No screenshot attachment renderer found'));
- });
- }
- }, [currentEventAttachment, AttachmentComponent]);
+ const AttachmentComponent =
+ getImageAttachmentRenderer(currentEventAttachment) ?? ImageViewer;
return (
diff --git a/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.spec.tsx b/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.spec.tsx
new file mode 100644
index 00000000000000..3ffbca8022b7af
--- /dev/null
+++ b/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.spec.tsx
@@ -0,0 +1,44 @@
+import {EventFixture} from 'sentry-fixture/event';
+import {EventAttachmentFixture} from 'sentry-fixture/eventAttachment';
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import {ScreenshotDataSection} from 'sentry/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection';
+import ProjectsStore from 'sentry/stores/projectsStore';
+
+describe('ScreenshotDataSection', function () {
+ const organization = OrganizationFixture({
+ features: ['event-attachments'],
+ orgRole: 'member',
+ attachmentsRole: 'member',
+ });
+ const project = ProjectFixture();
+ const event = EventFixture();
+
+ beforeEach(() => {
+ ProjectsStore.loadInitialData([project]);
+ });
+
+ it('renders without error when screenshot has application/json mimetype', async function () {
+ const attachment = EventAttachmentFixture({
+ name: 'screenshot.png',
+ mimetype: 'application/json',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/projects/${organization.slug}/${project.slug}/events/${event.id}/attachments/`,
+ body: [attachment],
+ });
+
+ render(, {
+ organization,
+ });
+
+ expect(await screen.findByTestId('image-viewer')).toBeInTheDocument();
+ });
+});
diff --git a/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.tsx b/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.tsx
index d0d23a7d2a8d54..b7ba96ebd43e2d 100644
--- a/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.tsx
+++ b/static/app/components/events/eventTagsAndScreenshot/screenshot/screenshotDataSection.tsx
@@ -47,12 +47,17 @@ export function ScreenshotDataSection({
},
{enabled: !isShare}
);
+ const [screenshotInFocus, setScreenshotInFocus] = useState(0);
const {mutate: deleteAttachment} = useDeleteEventAttachmentOptimistic();
- const screenshots = attachments?.filter(({name}) => name.includes('screenshot')) ?? [];
+ const screenshots = attachments?.filter(attachment =>
+ attachment.name.includes('screenshot')
+ );
- const [screenshotInFocus, setScreenshotInFocus] = useState(0);
+ const showScreenshot = !isShare && !!screenshots?.length;
+ if (!showScreenshot) {
+ return null;
+ }
- const showScreenshot = !isShare && !!screenshots.length;
const screenshot = screenshots[screenshotInFocus]!;
const handleDeleteScreenshot = (attachmentId: string) => {
diff --git a/static/app/views/issueDetails/groupEventAttachments/inlineEventAttachment.spec.tsx b/static/app/views/issueDetails/groupEventAttachments/inlineEventAttachment.spec.tsx
new file mode 100644
index 00000000000000..61ecd19599567c
--- /dev/null
+++ b/static/app/views/issueDetails/groupEventAttachments/inlineEventAttachment.spec.tsx
@@ -0,0 +1,70 @@
+import {EventFixture} from 'sentry-fixture/event';
+import {EventAttachmentFixture} from 'sentry-fixture/eventAttachment';
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import {InlineEventAttachment} from 'sentry/views/issueDetails/groupEventAttachments/inlineEventAttachment';
+
+describe('InlineEventAttachment', function () {
+ const organization = OrganizationFixture();
+ const project = ProjectFixture();
+ const event = EventFixture();
+
+ it('renders rrweb viewer for rrweb.json attachment', function () {
+ const attachment = EventAttachmentFixture({
+ name: 'rrweb.json',
+ mimetype: 'application/json',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/projects/${project.slug}/events/${event.id}/attachments/${attachment.id}/?download`,
+ body: '{"events": []}',
+ });
+
+ render(
+
+ );
+
+ expect(
+ screen.getByText(/This is an attachment containing a session replay/)
+ ).toBeInTheDocument();
+ expect(screen.getByRole('link', {name: 'View the replay'})).toBeInTheDocument();
+ });
+
+ it('renders rrweb viewer for rrweb- prefixed attachment', function () {
+ const attachment = EventAttachmentFixture({
+ name: 'rrweb-12345.json',
+ mimetype: 'application/json',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/projects/${project.slug}/events/${event.id}/attachments/${attachment.id}/?download`,
+ body: '{"events": []}',
+ });
+
+ render(
+
+ );
+
+ expect(
+ screen.getByText(/This is an attachment containing a session replay/)
+ ).toBeInTheDocument();
+ expect(screen.getByRole('link', {name: 'View the replay'})).toBeInTheDocument();
+ });
+});