diff --git a/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData.tsx b/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData.tsx index a3912babd0a5a9..707b285e3b7e4c 100644 --- a/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData.tsx +++ b/static/app/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData.tsx @@ -6,7 +6,7 @@ import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient'; import type {RequestError} from 'sentry/utils/requestError/requestError'; import {useOrganization} from 'sentry/utils/useOrganization'; -interface SourceMapDebugBlueThunderResponseFrame { +export interface SourceMapDebugBlueThunderResponseFrame { debug_id_process: { debug_id: string | null; uploaded_source_file_with_correct_debug_id: boolean; diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/diagnosisSection.spec.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/diagnosisSection.spec.tsx new file mode 100644 index 00000000000000..7e1011c021f627 --- /dev/null +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/diagnosisSection.spec.tsx @@ -0,0 +1,287 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; +import { + SourceMapDebugFrameFixture, + SourceMapDebugReleaseProcessFixture, + SourceMapDebugResponseFixture, +} from 'sentry-fixture/sourceMapDebug'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {textWithMarkupMatcher} from 'sentry-test/utils'; + +import {useSourceMapDebugQuery} from 'sentry/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData'; + +import {DiagnosisSection} from './diagnosisSection'; + +const PROJECT_SLUG = 'project-slug'; +const EVENT_ID = 'event-abc123'; + +function TestWrapper() { + const sourceMapQuery = useSourceMapDebugQuery( + PROJECT_SLUG, + EVENT_ID, + 'sentry.javascript.browser' + ); + return ; +} + +describe('DiagnosisSection', () => { + const organization = OrganizationFixture(); + const apiUrl = `/projects/${organization.slug}/${PROJECT_SLUG}/events/${EVENT_ID}/source-map-debug-blue-thunder-edition/`; + + it('shows error state when the API call fails', async () => { + MockApiClient.addMockResponse({url: apiUrl, statusCode: 500, body: {}}); + + render(, {organization}); + + expect( + await screen.findByText( + 'Unable to load source map diagnostic information for this event.' + ) + ).toBeInTheDocument(); + }); + + it('shows upload instructions when no artifacts exist for the project', async () => { + MockApiClient.addMockResponse({url: apiUrl, body: SourceMapDebugResponseFixture()}); + + render(, {organization}); + + expect( + await screen.findByText( + 'No source map artifacts have been uploaded for this project.' + ) + ).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Upload Instructions'})).toHaveAttribute( + 'href', + 'https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/' + ); + }); + + it('includes the release in the no-artifacts message when a release is set', async () => { + MockApiClient.addMockResponse({ + url: apiUrl, + body: SourceMapDebugResponseFixture({release: '2.5.0'}), + }); + + render(, {organization}); + + expect( + await screen.findByText( + textWithMarkupMatcher( + 'No source map artifacts have been uploaded for this project in release 2.5.0.' + ) + ) + ).toBeInTheDocument(); + }); + + it('shows dist mismatch message when the source file dist does not match', async () => { + MockApiClient.addMockResponse({ + url: apiUrl, + body: SourceMapDebugResponseFixture({ + project_has_some_artifact_bundle: true, + exceptions: [ + { + frames: [ + SourceMapDebugFrameFixture({ + release_process: SourceMapDebugReleaseProcessFixture({ + source_file_lookup_result: 'wrong-dist', + }), + }), + ], + }, + ], + }), + }); + + render(, {organization}); + + expect( + await screen.findByText( + textWithMarkupMatcher( + 'The source file ~/static/app.min.js was found but the dist value does not match the uploaded artifact.' + ) + ) + ).toBeInTheDocument(); + }); + + it('shows dist mismatch message when the source map dist does not match', async () => { + MockApiClient.addMockResponse({ + url: apiUrl, + body: SourceMapDebugResponseFixture({ + project_has_some_artifact_bundle: true, + exceptions: [ + { + frames: [ + SourceMapDebugFrameFixture({ + release_process: SourceMapDebugReleaseProcessFixture({ + source_map_reference: 'app.min.js.map', + source_map_lookup_result: 'wrong-dist', + }), + }), + ], + }, + ], + }), + }); + + render(, {organization}); + + expect( + await screen.findByText( + textWithMarkupMatcher( + 'The source map app.min.js.map was found but the dist value does not match the uploaded artifact.' + ) + ) + ).toBeInTheDocument(); + }); + + it('shows not-found message when the source file is missing and has no source map reference', async () => { + MockApiClient.addMockResponse({ + url: apiUrl, + body: SourceMapDebugResponseFixture({ + project_has_some_artifact_bundle: true, + exceptions: [ + { + frames: [ + SourceMapDebugFrameFixture({ + release_process: SourceMapDebugReleaseProcessFixture({ + source_file_lookup_result: 'unsuccessful', + }), + }), + ], + }, + ], + }), + }); + + render(, {organization}); + + expect( + await screen.findByText( + textWithMarkupMatcher( + 'The source file ~/static/app.min.js could not be found in any uploaded artifact bundle. No source map reference was detected.' + ) + ) + ).toBeInTheDocument(); + }); + + it('shows not-found message when the source map reference cannot be resolved', async () => { + MockApiClient.addMockResponse({ + url: apiUrl, + body: SourceMapDebugResponseFixture({ + project_has_some_artifact_bundle: true, + exceptions: [ + { + frames: [ + SourceMapDebugFrameFixture({ + release_process: SourceMapDebugReleaseProcessFixture({ + source_map_reference: 'app.min.js.map', + source_map_lookup_result: 'unsuccessful', + }), + }), + ], + }, + ], + }), + }); + + render(, {organization}); + + expect( + await screen.findByText( + textWithMarkupMatcher( + 'The source map referenced by ~/static/app.min.js points to app.min.js.map, but no matching artifact was found.' + ) + ) + ).toBeInTheDocument(); + }); + + it('shows scraping failure message when Sentry cannot fetch the source file', async () => { + MockApiClient.addMockResponse({ + url: apiUrl, + body: SourceMapDebugResponseFixture({ + project_has_some_artifact_bundle: true, + has_scraping_data: true, + exceptions: [ + { + frames: [ + SourceMapDebugFrameFixture({ + scraping_process: { + source_file: { + status: 'failure', + url: 'https://example.com/app.js', + reason: 'not_found', + }, + source_map: null, + }, + }), + ], + }, + ], + }), + }); + + render(, {organization}); + + expect( + await screen.findByText( + textWithMarkupMatcher( + 'Sentry could not fetch the source file at https://example.com/app.js: not_found.' + ) + ) + ).toBeInTheDocument(); + }); + + it('shows scraping failure message when Sentry cannot fetch the source map', async () => { + MockApiClient.addMockResponse({ + url: apiUrl, + body: SourceMapDebugResponseFixture({ + project_has_some_artifact_bundle: true, + has_scraping_data: true, + exceptions: [ + { + frames: [ + SourceMapDebugFrameFixture({ + scraping_process: { + source_file: {status: 'success', url: 'https://example.com/app.js'}, + source_map: { + status: 'failure', + url: 'https://example.com/app.js.map', + reason: 'timeout', + }, + }, + }), + ], + }, + ], + }), + }); + + render(, {organization}); + + expect( + await screen.findByText( + textWithMarkupMatcher( + 'Sentry could not fetch the source map at https://example.com/app.js.map: timeout.' + ) + ) + ).toBeInTheDocument(); + }); + + it('shows fallback message when source maps are present but issue cannot be pinpointed', async () => { + MockApiClient.addMockResponse({ + url: apiUrl, + body: SourceMapDebugResponseFixture({ + project_has_some_artifact_bundle: true, + release_has_some_artifact: true, + }), + }); + + render(, {organization}); + + expect( + await screen.findByText( + 'Source maps appear to be configured but Sentry could not pinpoint the exact issue.' + ) + ).toBeInTheDocument(); + }); +}); diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/problemSection.spec.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/problemSection.spec.tsx new file mode 100644 index 00000000000000..d5dfa5b2768974 --- /dev/null +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/problemSection.spec.tsx @@ -0,0 +1,19 @@ +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {ProblemSection} from './problemSection'; + +describe('ProblemSection', () => { + it('renders title, description, and docs link', () => { + render(); + + expect(screen.getByText('Problem')).toBeInTheDocument(); + expect( + screen.getByText( + "Your source maps aren't configured correctly, so stack traces will show minified code instead of your original source. Fix this to see the exact file, line, and function causing the error." + ) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', {name: 'Why configure source maps?'}) + ).toHaveAttribute('href', 'https://docs.sentry.io/platforms/javascript/sourcemaps/'); + }); +}); diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/troubleshootingSection.spec.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/troubleshootingSection.spec.tsx new file mode 100644 index 00000000000000..2caa09f28d195b --- /dev/null +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/troubleshootingSection.spec.tsx @@ -0,0 +1,44 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ProjectFixture} from 'sentry-fixture/project'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {TroubleshootingSection} from './troubleshootingSection'; + +describe('TroubleshootingSection', () => { + const organization = OrganizationFixture(); + const project = ProjectFixture({slug: 'my-project'}); + + it('renders all troubleshooting step titles', () => { + render(, {organization}); + + expect(screen.getByText('Verify Artifacts Are Uploaded')).toBeInTheDocument(); + expect( + screen.getByText("Verify That You're Building Source Maps") + ).toBeInTheDocument(); + expect( + screen.getByText("Verify That You're Running a Production Build") + ).toBeInTheDocument(); + expect( + screen.getByText('Verify Your Source Files Contain Debug ID Injection Snippets') + ).toBeInTheDocument(); + }); + + it('settings link points to the correct project source maps URL', () => { + render(, {organization}); + + expect(screen.getByRole('button', {name: /settings/i})).toHaveAttribute( + 'href', + `/settings/${organization.slug}/projects/${project.slug}/source-maps/` + ); + }); + + it('renders the footer docs link', () => { + render(, {organization}); + + expect(screen.getByRole('link', {name: /read all documentation/i})).toHaveAttribute( + 'href', + 'https://docs.sentry.io/platforms/javascript/sourcemaps/troubleshooting_js/' + ); + }); +}); diff --git a/tests/js/fixtures/sourceMapDebug.ts b/tests/js/fixtures/sourceMapDebug.ts new file mode 100644 index 00000000000000..1f4793d74e3250 --- /dev/null +++ b/tests/js/fixtures/sourceMapDebug.ts @@ -0,0 +1,56 @@ +import type { + SourceMapDebugBlueThunderResponse, + SourceMapDebugBlueThunderResponseFrame, +} from 'sentry/components/events/interfaces/crashContent/exception/useSourceMapDebuggerData'; + +type ReleaseProcess = NonNullable< + SourceMapDebugBlueThunderResponseFrame['release_process'] +>; + +export function SourceMapDebugReleaseProcessFixture( + params: Partial = {} +): ReleaseProcess { + return { + abs_path: '~/static/app.min.js', + matching_source_file_names: [], + matching_source_map_name: null, + source_file_lookup_result: 'found', + source_map_lookup_result: 'found', + source_map_reference: null, + ...params, + }; +} + +export function SourceMapDebugFrameFixture( + params: Partial = {} +): SourceMapDebugBlueThunderResponseFrame { + return { + debug_id_process: { + debug_id: null, + uploaded_source_file_with_correct_debug_id: false, + uploaded_source_map_with_correct_debug_id: false, + }, + release_process: SourceMapDebugReleaseProcessFixture(), + scraping_process: {source_file: null, source_map: null}, + ...params, + }; +} + +export function SourceMapDebugResponseFixture( + params: Partial = {} +): SourceMapDebugBlueThunderResponse { + return { + dist: null, + release: null, + exceptions: [], + has_debug_ids: false, + min_debug_id_sdk_version: null, + sdk_version: null, + project_has_some_artifact_bundle: false, + release_has_some_artifact: false, + has_uploaded_some_artifact_with_a_debug_id: false, + sdk_debug_id_support: 'not-supported', + has_scraping_data: false, + ...params, + }; +}