diff --git a/static/app/components/events/interfaces/crashContent/exception/androidNativeTombstonesBanner.tsx b/static/app/components/events/interfaces/crashContent/exception/androidNativeTombstonesBanner.tsx new file mode 100644 index 00000000000000..425914e0e7bd5e --- /dev/null +++ b/static/app/components/events/interfaces/crashContent/exception/androidNativeTombstonesBanner.tsx @@ -0,0 +1,247 @@ +import {useState} from 'react'; +import styled from '@emotion/styled'; + +import {LinkButton} from '@sentry/scraps/button'; +import {CodeBlock} from '@sentry/scraps/code'; +import {Flex} from '@sentry/scraps/layout'; +import {Heading, Text} from '@sentry/scraps/text'; + +import {usePrompt} from 'sentry/actionCreators/prompts'; +import {DropdownMenu} from 'sentry/components/dropdownMenu'; +import {IconClose} from 'sentry/icons'; +import {t} from 'sentry/locale'; +import type {EntryException, Event, ExceptionValue} from 'sentry/types/event'; +import {EntryType} from 'sentry/types/event'; +import {trackAnalytics} from 'sentry/utils/analytics'; +import {useOrganization} from 'sentry/utils/useOrganization'; + +const TOMBSTONES_DOCS_URL = + 'https://docs.sentry.io/platforms/android/configuration/tombstones/'; + +type TabConfig = { + code: string; + label: string; + language: string; + value: string; +}; + +type SdkConfig = { + defaultTab: string; + tabs: TabConfig[]; +}; + +const ANDROID_SDK_CONFIG: SdkConfig = { + defaultTab: 'manifest', + tabs: [ + { + label: 'AndroidManifest.xml', + value: 'manifest', + language: 'xml', + code: ` + + +`, + }, + { + label: 'Kotlin', + value: 'kotlin', + language: 'kotlin', + code: `// Requires sentry-android 8.35.0+ +SentryAndroid.init(context) { options -> + options.isTombstoneEnabled = true +}`, + }, + { + label: 'Java', + value: 'java', + language: 'java', + code: `// Requires sentry-android 8.35.0+ +SentryAndroid.init(context, options -> { + options.setTombstoneEnabled(true); +});`, + }, + ], +}; + +const REACT_NATIVE_SDK_CONFIG: SdkConfig = { + defaultTab: 'javascript', + tabs: [ + { + label: 'JavaScript', + value: 'javascript', + language: 'javascript', + code: `// Requires @sentry/react-native 8.5.0+ +Sentry.init({ + enableTombstone: true, +});`, + }, + ], +}; + +const FLUTTER_SDK_CONFIG: SdkConfig = { + defaultTab: 'dart', + tabs: [ + { + label: 'Dart', + value: 'dart', + language: 'dart', + code: `// Requires sentry_flutter 9.15.0+ +await SentryFlutter.init( + (options) { + options.enableTombstone = true; + }, +);`, + }, + ], +}; + +const SDK_CONFIGS: Record = { + 'sentry.native.android': ANDROID_SDK_CONFIG, + 'sentry.native.android.react-native': REACT_NATIVE_SDK_CONFIG, + 'sentry.native.android.flutter': FLUTTER_SDK_CONFIG, +}; + +function hasSignalHandlerMechanism(event: Event): boolean { + const exceptionEntry = event.entries?.find( + (entry): entry is EntryException => entry.type === EntryType.EXCEPTION + ); + if (!exceptionEntry) { + return false; + } + return ( + exceptionEntry.data.values?.some( + (value: ExceptionValue) => value.mechanism?.type === 'signalhandler' + ) ?? false + ); +} + +function getSdkConfig(event: Event): SdkConfig | undefined { + const sdkName = event.sdk?.name; + if (!sdkName) { + return undefined; + } + return SDK_CONFIGS[sdkName]; +} + +export function shouldShowTombstonesBanner(event: Event): boolean { + return getSdkConfig(event) !== undefined && hasSignalHandlerMechanism(event); +} + +interface Props { + event: Event; + projectId: string; +} + +export function AndroidNativeTombstonesBanner({event, projectId}: Props) { + const organization = useOrganization(); + const sdkConfig = getSdkConfig(event); + const [codeTab, setCodeTab] = useState(sdkConfig?.defaultTab ?? 'manifest'); + + const {isLoading, isError, isPromptDismissed, dismissPrompt, snoozePrompt} = usePrompt({ + feature: 'issue_android_tombstones_onboarding', + organization, + projectId, + daysToSnooze: 7, + }); + + if (isLoading || isError || isPromptDismissed || !sdkConfig) { + return null; + } + + const activeTab = + sdkConfig.tabs.find(tab => tab.value === codeTab) ?? sdkConfig.tabs[0]!; + + return ( + + + {t('Enable Tombstone Collection')} + + {t( + 'This native crash was captured via the Android NDK integration only. Enable Tombstone collection in your application to get richer crash reports with more context, including additional thread information, better stack traces and more.' + )} + + ({label: tab.label, value: tab.value}))} + selectedTab={codeTab} + onTabClick={setCodeTab} + language={activeTab.language} + > + {activeTab.code} + + + {t('Learn More')} + + + , + }} + size="xs" + items={[ + { + key: 'dismiss', + label: t('Dismiss'), + onAction: () => { + dismissPrompt(); + trackAnalytics('issue-details.android-tombstones-cta-dismiss', { + organization, + type: 'dismiss', + }); + }, + }, + { + key: 'snooze', + label: t('Snooze'), + onAction: () => { + snoozePrompt(); + trackAnalytics('issue-details.android-tombstones-cta-dismiss', { + organization, + type: 'snooze', + }); + }, + }, + ]} + /> + + ); +} + +const BannerWrapper = styled('div')` + position: relative; + border: 1px solid ${p => p.theme.tokens.border.primary}; + border-radius: ${p => p.theme.radius.md}; + padding: ${p => p.theme.space.xl}; + margin: ${p => p.theme.space.md} 0; + background: linear-gradient( + 90deg, + color-mix(in srgb, ${p => p.theme.tokens.background.secondary} 0%, transparent) 0%, + ${p => p.theme.tokens.background.secondary} 70%, + ${p => p.theme.tokens.background.secondary} 100% + ); +`; + +const CloseDropdownMenu = styled(DropdownMenu)` + position: absolute; + display: block; + top: ${p => p.theme.space.md}; + right: ${p => p.theme.space.md}; + cursor: pointer; + z-index: 1; +`; diff --git a/static/app/utils/analytics/issueAnalyticsEvents.tsx b/static/app/utils/analytics/issueAnalyticsEvents.tsx index 7b00e12d35650d..cf54f00f0070bd 100644 --- a/static/app/utils/analytics/issueAnalyticsEvents.tsx +++ b/static/app/utils/analytics/issueAnalyticsEvents.tsx @@ -72,6 +72,7 @@ export type IssueEventParameters = { 'integrations.integration_reinstall_clicked': { provider: string; }; + 'issue-details.android-tombstones-cta-dismiss': {type: string}; 'issue-details.replay-cta-dismiss': {type: string}; 'issue.engaged_view': { group_id: number; @@ -388,6 +389,8 @@ export const issueEventMap: Record = { 'issue_details.event_dropdown_option_selected': 'Issue Details: Event Dropdown Option Selected', 'issue_details.header_view_replay_clicked': 'Issue Details: Header View Replay Clicked', + 'issue-details.android-tombstones-cta-dismiss': + 'Issue Details Android Tombstones CTA Dismissed', 'issue-details.replay-cta-dismiss': 'Issue Details Replay CTA Dismissed', 'issue_group_details.anr_root_cause_detected': 'Detected ANR Root Cause', 'issue_details.copy_issue_details_as_markdown': diff --git a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx index 42629448e60edc..09b07113fca77f 100644 --- a/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx +++ b/static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx @@ -36,6 +36,10 @@ import {EventGroupingInfoSection} from 'sentry/components/events/groupingInfo/gr import {HighlightsDataSection} from 'sentry/components/events/highlights/highlightsDataSection'; import {HighlightsIconSummary} from 'sentry/components/events/highlights/highlightsIconSummary'; import {ActionableItems} from 'sentry/components/events/interfaces/crashContent/exception/actionableItems'; +import { + AndroidNativeTombstonesBanner, + shouldShowTombstonesBanner, +} from 'sentry/components/events/interfaces/crashContent/exception/androidNativeTombstonesBanner'; import {actionableItemsEnabled} from 'sentry/components/events/interfaces/crashContent/exception/useActionableItems'; import {Csp} from 'sentry/components/events/interfaces/csp'; import {DebugMeta} from 'sentry/components/events/interfaces/debugMeta'; @@ -90,7 +94,7 @@ import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSectio import {MetricDetectorTriggeredSection} from 'sentry/views/issueDetails/streamline/sidebar/metricDetectorTriggeredSection'; import {SizeAnalysisTriggeredSection} from 'sentry/views/issueDetails/streamline/sidebar/sizeAnalysisTriggeredSection'; import {TraceDataSection} from 'sentry/views/issueDetails/traceDataSection'; -import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils'; +import {useHasStreamlinedUI, useIsSampleEvent} from 'sentry/views/issueDetails/utils'; import {DEFAULT_TRACE_VIEW_PREFERENCES} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences'; import {TraceStateProvider} from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider'; @@ -131,6 +135,7 @@ export function EventDetailsContent({ : null; const isMetricKitHang = hangProfileData !== null; const groupingCurrentLevel = group?.metadata?.current_level; + const isSampleError = useIsSampleEvent(); const hasActionableItems = actionableItemsEnabled({ eventId: event.id, @@ -260,6 +265,14 @@ export function EventDetailsContent({ display: block !important; `} > + {shouldShowTombstonesBanner(event) && !isSampleError && ( + + + + )} {defined(eventEntries[EntryType.EXCEPTION]) && ( {shouldUseNewStackTrace ? (