From f26146e983c987ca0fe551daba0c126f3888d711 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 27 May 2025 08:43:48 +0200 Subject: [PATCH] feat(source-maps): Introduce new empty state copies and react-native callout --- .../interfaces/sourceMapsDebuggerModal.tsx | 18 ++- .../projectSourceMaps/sourceMapsList.spec.tsx | 2 +- .../projectSourceMaps/sourceMapsList.tsx | 123 ++++++++++++++++-- 3 files changed, 132 insertions(+), 11 deletions(-) diff --git a/static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx b/static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx index b8798578738206..4d4a0fdae3881d 100644 --- a/static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx +++ b/static/app/components/events/interfaces/sourceMapsDebuggerModal.tsx @@ -158,7 +158,7 @@ export interface SourceMapsDebuggerModalProps extends ModalRenderProps { orgSlug?: string; } -const projectPlatformToDocsMap: Record = { +export const projectPlatformToDocsMap: Record = { 'node-azurefunctions': 'azure-functions', 'node-cloudflare-pages': 'cloudflare', 'node-cloudflare-workers': 'cloudflare', @@ -171,6 +171,20 @@ const projectPlatformToDocsMap: Record = { 'node-nestjs': 'nestjs', 'node-restify': 'restify', 'node-awslambda': 'aws-lambda', + 'javascript-react': 'react', + 'javascript-angular': 'angular', + 'javascript-ember': 'ember', + 'javascript-gatsby': 'gatsby', + 'javascript-vue': 'vue', + 'javascript-nextjs': 'nextjs', + 'javascript-nuxt': 'nuxt', + 'javascript-remix': 'remix', + 'javascript-solid': 'solid', + 'javascript-solidstart': 'solidstart', + 'javascript-svelte': 'svelte', + 'javascript-sveltekit': 'sveltekit', + 'javascript-astro': 'astro', + 'javascript-tanstackstart-react': 'tanstackstart-react', }; function isReactNativeSDK({sdkName}: Pick) { @@ -191,7 +205,7 @@ function getPlatform({ ); } -function getSourceMapsDocLinks(platform: string) { +export function getSourceMapsDocLinks(platform: string) { if (platform === 'react-native') { return { sourcemaps: `https://docs.sentry.io/platforms/react-native/sourcemaps/`, diff --git a/static/app/views/settings/projectSourceMaps/sourceMapsList.spec.tsx b/static/app/views/settings/projectSourceMaps/sourceMapsList.spec.tsx index 8c5e9f6e2b3633..2618dc76960642 100644 --- a/static/app/views/settings/projectSourceMaps/sourceMapsList.spec.tsx +++ b/static/app/views/settings/projectSourceMaps/sourceMapsList.spec.tsx @@ -169,7 +169,7 @@ describe('ProjectSourceMaps', function () { organization, }); - expect(await screen.findByText('No source map uploads found')).toBeInTheDocument(); + expect(await screen.findByText('No source maps uploaded')).toBeInTheDocument(); }); }); }); diff --git a/static/app/views/settings/projectSourceMaps/sourceMapsList.tsx b/static/app/views/settings/projectSourceMaps/sourceMapsList.tsx index 8dd21337ae113c..ee14d10d96e1f4 100644 --- a/static/app/views/settings/projectSourceMaps/sourceMapsList.tsx +++ b/static/app/views/settings/projectSourceMaps/sourceMapsList.tsx @@ -1,13 +1,19 @@ import {Fragment, useCallback, useMemo, useState} from 'react'; +import {css} from '@emotion/react'; import styled from '@emotion/styled'; import Access from 'sentry/components/acl/access'; +import {CodeSnippet} from 'sentry/components/codeSnippet'; import Confirm from 'sentry/components/confirm'; import {Button, type ButtonProps} from 'sentry/components/core/button'; import {Tooltip} from 'sentry/components/core/tooltip'; import {DateTime} from 'sentry/components/dateTime'; import EmptyMessage from 'sentry/components/emptyMessage'; import KeyValueList from 'sentry/components/events/interfaces/keyValueList'; +import { + getSourceMapsDocLinks, + projectPlatformToDocsMap, +} from 'sentry/components/events/interfaces/sourceMapsDebuggerModal'; import ExternalLink from 'sentry/components/links/externalLink'; import Link from 'sentry/components/links/link'; import LoadingIndicator from 'sentry/components/loadingIndicator'; @@ -24,6 +30,7 @@ import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import type {SourceMapsArchive} from 'sentry/types/release'; import type {DebugIdBundle, DebugIdBundleAssociation} from 'sentry/types/sourceMaps'; +import {defined} from 'sentry/utils'; import {keepPreviousData, useApiQuery} from 'sentry/utils/queryClient'; import {decodeScalar} from 'sentry/utils/queryString'; import useOrganization from 'sentry/utils/useOrganization'; @@ -135,7 +142,7 @@ function useSourceMapUploads({ export function SourceMapsList({location, router, project}: Props) { const organization = useOrganization(); - const query = decodeScalar(location.query.query); + const query = decodeScalar(location.query.query) ?? ''; const cursor = location.query.cursor ?? ''; @@ -165,6 +172,12 @@ export function SourceMapsList({location, router, project}: Props) { [router, location] ); + const platformByProject = defined(project.platform) + ? projectPlatformToDocsMap[project.platform] + : undefined; + const platform = platformByProject ?? project.platform ?? 'javascript'; + const sourceMapsLinks = getSourceMapsDocLinks(platform); + return ( @@ -172,9 +185,7 @@ export function SourceMapsList({location, router, project}: Props) { {tct( `These source map archives help Sentry identify where to look when code is minified. By providing this information, you can get better context for your stack traces when debugging. To learn more about source maps, [link: read the docs].`, { - link: ( - - ), + link: , } )} @@ -187,30 +198,119 @@ export function SourceMapsList({location, router, project}: Props) { project={project} sourceMapUploads={sourceMapUploads} isLoading={isPending} - emptyMessage={t('No source map uploads found')} + query={query} + onClearSearch={() => handleSearch('')} onDelete={id => { deleteSourceMaps({bundleId: id, projectSlug: project.slug}); }} + docsLink={sourceMapsLinks.sourcemaps} /> ); } +function ReactNativeCallOut() { + const [selectedTab, setSelectedTab] = useState('expo'); + + return ( +
+
+ {tct( + 'For React Native projects, ensure you [strong:run the app in release mode] and execute the scripts below to upload source maps for both iOS and Android:', + {strong: } + )} +
+ setSelectedTab(value)} + > + {selectedTab === 'expo' + ? '# First run this to create a build and upload source maps\n./gradlew assembleRelease\n# Then run this to test your build locally\nnpx expo run:android --variant release\n\n# iOS version (pending confirmation)\nnpx expo run:ios --configuration Release' + : 'npx react-native run-android --mode release\nnpx react-native run-ios --mode Release'} + +
+ ); +} + +interface SourceMapsEmptyStateProps { + docsLink: string; + onClearSearch: () => void; + project: Project; + query?: string; +} + +function SourceMapsEmptyState({ + query, + onClearSearch, + project, + docsLink, +}: SourceMapsEmptyStateProps) { + return ( + + + ), + } + ) + : tct( + 'Source maps allow Sentry to map your production code to your source code. See our [docs:docs] to learn more about configuring your application to upload source maps to Sentry.', + { + docs: , + } + ) + } + action={project.platform === 'react-native' ? : undefined} + /> + + ); +} + interface SourceMapUploadsListProps { - emptyMessage: React.ReactNode; + docsLink: string; isLoading: boolean; + onClearSearch: () => void; onDelete: (id: string) => void; project: Project; + query?: string; sourceMapUploads?: SourceMapUpload[]; } function SourceMapUploadsList({ + onClearSearch, isLoading, sourceMapUploads, - emptyMessage, onDelete, project, + query, + docsLink, }: SourceMapUploadsListProps) { const organization = useOrganization(); @@ -226,7 +326,14 @@ function SourceMapUploadsList({ } if (!sourceMapUploads || sourceMapUploads.length === 0) { - return {emptyMessage}; + return ( + + ); } return (