|
| 1 | +import {useState} from 'react'; |
| 2 | +import styled from '@emotion/styled'; |
| 3 | + |
| 4 | +import {LinkButton} from '@sentry/scraps/button'; |
| 5 | +import {CodeBlock} from '@sentry/scraps/code'; |
| 6 | + |
| 7 | +import {usePrompt} from 'sentry/actionCreators/prompts'; |
| 8 | +import {DropdownMenu} from 'sentry/components/dropdownMenu'; |
| 9 | +import {IconClose} from 'sentry/icons'; |
| 10 | +import {t} from 'sentry/locale'; |
| 11 | +import type {EntryException, Event} from 'sentry/types/event'; |
| 12 | +import {EntryType} from 'sentry/types/event'; |
| 13 | +import {trackAnalytics} from 'sentry/utils/analytics'; |
| 14 | +import {useOrganization} from 'sentry/utils/useOrganization'; |
| 15 | +import {SectionKey} from 'sentry/views/issueDetails/streamline/context'; |
| 16 | +import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection'; |
| 17 | + |
| 18 | +const ANDROID_NATIVE_SDK_PREFIX = 'sentry.native.android'; |
| 19 | +const TOMBSTONES_DOCS_URL = |
| 20 | + 'https://docs.sentry.io/platforms/android/configuration/tombstones/'; |
| 21 | + |
| 22 | +const CODE_SNIPPETS: Record<string, string> = { |
| 23 | + manifest: `<application> |
| 24 | + <meta-data |
| 25 | + android:name="io.sentry.tombstone.enable" |
| 26 | + android:value="true" /> |
| 27 | +</application>`, |
| 28 | + kotlin: `SentryAndroid.init(context) { options -> |
| 29 | + options.isReportHistoricalTombstones = true |
| 30 | +}`, |
| 31 | + java: `SentryAndroid.init(context, options -> { |
| 32 | + options.setReportHistoricalTombstones(true); |
| 33 | +});`, |
| 34 | +}; |
| 35 | + |
| 36 | +function hasSignalHandlerMechanism(event: Event): boolean { |
| 37 | + const exceptionEntry = event.entries?.find( |
| 38 | + (entry): entry is EntryException => entry.type === EntryType.EXCEPTION |
| 39 | + ); |
| 40 | + if (!exceptionEntry) { |
| 41 | + return false; |
| 42 | + } |
| 43 | + return ( |
| 44 | + exceptionEntry.data.values?.some( |
| 45 | + value => value.mechanism?.type === 'signalhandler' |
| 46 | + ) ?? false |
| 47 | + ); |
| 48 | +} |
| 49 | + |
| 50 | +function isAndroidNativeSdk(event: Event): boolean { |
| 51 | + return event.sdk?.name?.startsWith(ANDROID_NATIVE_SDK_PREFIX) ?? false; |
| 52 | +} |
| 53 | + |
| 54 | +export function shouldShowTombstonesBanner(event: Event): boolean { |
| 55 | + return isAndroidNativeSdk(event) && hasSignalHandlerMechanism(event); |
| 56 | +} |
| 57 | + |
| 58 | +interface Props { |
| 59 | + event: Event; |
| 60 | + projectId: string; |
| 61 | +} |
| 62 | + |
| 63 | +export function AndroidNativeTombstonesBanner({event, projectId}: Props) { |
| 64 | + const organization = useOrganization(); |
| 65 | + const [codeTab, setCodeTab] = useState('manifest'); |
| 66 | + |
| 67 | + const {isLoading, isError, isPromptDismissed, dismissPrompt, snoozePrompt} = usePrompt({ |
| 68 | + feature: 'issue_android_tombstones_onboarding', |
| 69 | + organization, |
| 70 | + projectId, |
| 71 | + daysToSnooze: 7, |
| 72 | + }); |
| 73 | + |
| 74 | + if (isLoading || isError || isPromptDismissed) { |
| 75 | + return null; |
| 76 | + } |
| 77 | + |
| 78 | + return ( |
| 79 | + <InterimSection type={SectionKey.EXCEPTION} title={t('Improve Native Crash Reports')}> |
| 80 | + <BannerWrapper> |
| 81 | + <div> |
| 82 | + <BannerTitle>{t('Enable Tombstone Collection')}</BannerTitle> |
| 83 | + <BannerDescription> |
| 84 | + {t( |
| 85 | + '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.' |
| 86 | + )} |
| 87 | + </BannerDescription> |
| 88 | + <CodeBlock |
| 89 | + tabs={[ |
| 90 | + {label: 'AndroidManifest.xml', value: 'manifest'}, |
| 91 | + {label: 'Kotlin', value: 'kotlin'}, |
| 92 | + {label: 'Java', value: 'java'}, |
| 93 | + ]} |
| 94 | + selectedTab={codeTab} |
| 95 | + onTabClick={setCodeTab} |
| 96 | + language={codeTab === 'manifest' ? 'xml' : codeTab} |
| 97 | + > |
| 98 | + {CODE_SNIPPETS[codeTab]} |
| 99 | + </CodeBlock> |
| 100 | + <LinkButton |
| 101 | + style={{marginTop: '12px'}} |
| 102 | + href={TOMBSTONES_DOCS_URL} |
| 103 | + external |
| 104 | + priority="primary" |
| 105 | + size="sm" |
| 106 | + analyticsEventName="Clicked Android Tombstones Onboarding CTA" |
| 107 | + analyticsEventKey="issue-details.android-tombstones-onboarding-cta-clicked" |
| 108 | + analyticsParams={{ |
| 109 | + organization, |
| 110 | + sdk_name: event.sdk?.name ?? '', |
| 111 | + }} |
| 112 | + > |
| 113 | + {t('Learn More')} |
| 114 | + </LinkButton> |
| 115 | + </div> |
| 116 | + <CloseDropdownMenu |
| 117 | + position="bottom-end" |
| 118 | + triggerProps={{ |
| 119 | + showChevron: false, |
| 120 | + priority: 'transparent', |
| 121 | + icon: <IconClose variant="muted" />, |
| 122 | + }} |
| 123 | + size="xs" |
| 124 | + items={[ |
| 125 | + { |
| 126 | + key: 'dismiss', |
| 127 | + label: t('Dismiss'), |
| 128 | + onAction: () => { |
| 129 | + dismissPrompt(); |
| 130 | + trackAnalytics('issue-details.android-tombstones-cta-dismiss', { |
| 131 | + organization, |
| 132 | + type: 'dismiss', |
| 133 | + }); |
| 134 | + }, |
| 135 | + }, |
| 136 | + { |
| 137 | + key: 'snooze', |
| 138 | + label: t('Snooze'), |
| 139 | + onAction: () => { |
| 140 | + snoozePrompt(); |
| 141 | + trackAnalytics('issue-details.android-tombstones-cta-dismiss', { |
| 142 | + organization, |
| 143 | + type: 'snooze', |
| 144 | + }); |
| 145 | + }, |
| 146 | + }, |
| 147 | + ]} |
| 148 | + /> |
| 149 | + </BannerWrapper> |
| 150 | + </InterimSection> |
| 151 | + ); |
| 152 | +} |
| 153 | + |
| 154 | +const BannerWrapper = styled('div')` |
| 155 | + position: relative; |
| 156 | + border: 1px solid ${p => p.theme.tokens.border.primary}; |
| 157 | + border-radius: ${p => p.theme.radius.md}; |
| 158 | + padding: ${p => p.theme.space.xl}; |
| 159 | + margin: ${p => p.theme.space.md} 0; |
| 160 | + background: linear-gradient( |
| 161 | + 90deg, |
| 162 | + color-mix(in srgb, ${p => p.theme.tokens.background.secondary} 0%, transparent) 0%, |
| 163 | + ${p => p.theme.tokens.background.secondary} 70%, |
| 164 | + ${p => p.theme.tokens.background.secondary} 100% |
| 165 | + ); |
| 166 | +`; |
| 167 | + |
| 168 | +const BannerTitle = styled('div')` |
| 169 | + font-size: ${p => p.theme.font.size.xl}; |
| 170 | + margin-bottom: ${p => p.theme.space.md}; |
| 171 | + font-weight: ${p => p.theme.font.weight.sans.medium}; |
| 172 | +`; |
| 173 | + |
| 174 | +const BannerDescription = styled('div')` |
| 175 | + margin-bottom: ${p => p.theme.space.lg}; |
| 176 | + max-width: 460px; |
| 177 | +`; |
| 178 | + |
| 179 | +const CloseDropdownMenu = styled(DropdownMenu)` |
| 180 | + position: absolute; |
| 181 | + display: block; |
| 182 | + top: ${p => p.theme.space.md}; |
| 183 | + right: ${p => p.theme.space.md}; |
| 184 | + color: ${p => p.theme.colors.white}; |
| 185 | + cursor: pointer; |
| 186 | + z-index: 1; |
| 187 | +`; |
0 commit comments