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 && ( + + + , + '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(); + }); +});