|
| 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 | +import {Flex} from '@sentry/scraps/layout'; |
| 7 | +import {Heading, Text} from '@sentry/scraps/text'; |
| 8 | + |
| 9 | +import {usePrompt} from 'sentry/actionCreators/prompts'; |
| 10 | +import {DropdownMenu} from 'sentry/components/dropdownMenu'; |
| 11 | +import {IconClose} from 'sentry/icons'; |
| 12 | +import {t} from 'sentry/locale'; |
| 13 | +import type {EntryException, Event, ExceptionValue} from 'sentry/types/event'; |
| 14 | +import {EntryType} from 'sentry/types/event'; |
| 15 | +import {trackAnalytics} from 'sentry/utils/analytics'; |
| 16 | +import {useOrganization} from 'sentry/utils/useOrganization'; |
| 17 | + |
| 18 | +const TOMBSTONES_DOCS_URL = |
| 19 | + 'https://docs.sentry.io/platforms/android/configuration/tombstones/'; |
| 20 | + |
| 21 | +type TabConfig = { |
| 22 | + code: string; |
| 23 | + label: string; |
| 24 | + language: string; |
| 25 | + value: string; |
| 26 | +}; |
| 27 | + |
| 28 | +type SdkConfig = { |
| 29 | + defaultTab: string; |
| 30 | + tabs: TabConfig[]; |
| 31 | +}; |
| 32 | + |
| 33 | +const ANDROID_SDK_CONFIG: SdkConfig = { |
| 34 | + defaultTab: 'manifest', |
| 35 | + tabs: [ |
| 36 | + { |
| 37 | + label: 'AndroidManifest.xml', |
| 38 | + value: 'manifest', |
| 39 | + language: 'xml', |
| 40 | + code: `<!-- Requires sentry-android 8.35.0+ --> |
| 41 | +<application> |
| 42 | + <meta-data |
| 43 | + android:name="io.sentry.tombstone.enable" |
| 44 | + android:value="true" /> |
| 45 | +</application>`, |
| 46 | + }, |
| 47 | + { |
| 48 | + label: 'Kotlin', |
| 49 | + value: 'kotlin', |
| 50 | + language: 'kotlin', |
| 51 | + code: `// Requires sentry-android 8.35.0+ |
| 52 | +SentryAndroid.init(context) { options -> |
| 53 | + options.isTombstoneEnabled = true |
| 54 | +}`, |
| 55 | + }, |
| 56 | + { |
| 57 | + label: 'Java', |
| 58 | + value: 'java', |
| 59 | + language: 'java', |
| 60 | + code: `// Requires sentry-android 8.35.0+ |
| 61 | +SentryAndroid.init(context, options -> { |
| 62 | + options.setTombstoneEnabled(true); |
| 63 | +});`, |
| 64 | + }, |
| 65 | + ], |
| 66 | +}; |
| 67 | + |
| 68 | +const REACT_NATIVE_SDK_CONFIG: SdkConfig = { |
| 69 | + defaultTab: 'javascript', |
| 70 | + tabs: [ |
| 71 | + { |
| 72 | + label: 'JavaScript', |
| 73 | + value: 'javascript', |
| 74 | + language: 'javascript', |
| 75 | + code: `// Requires @sentry/react-native 8.5.0+ |
| 76 | +Sentry.init({ |
| 77 | + enableTombstone: true, |
| 78 | +});`, |
| 79 | + }, |
| 80 | + ], |
| 81 | +}; |
| 82 | + |
| 83 | +const FLUTTER_SDK_CONFIG: SdkConfig = { |
| 84 | + defaultTab: 'dart', |
| 85 | + tabs: [ |
| 86 | + { |
| 87 | + label: 'Dart', |
| 88 | + value: 'dart', |
| 89 | + language: 'dart', |
| 90 | + code: `// Requires sentry_flutter 9.15.0+ |
| 91 | +await SentryFlutter.init( |
| 92 | + (options) { |
| 93 | + options.enableTombstone = true; |
| 94 | + }, |
| 95 | +);`, |
| 96 | + }, |
| 97 | + ], |
| 98 | +}; |
| 99 | + |
| 100 | +const SDK_CONFIGS: Record<string, SdkConfig> = { |
| 101 | + 'sentry.native.android': ANDROID_SDK_CONFIG, |
| 102 | + 'sentry.native.android.react-native': REACT_NATIVE_SDK_CONFIG, |
| 103 | + 'sentry.native.android.flutter': FLUTTER_SDK_CONFIG, |
| 104 | +}; |
| 105 | + |
| 106 | +function hasSignalHandlerMechanism(event: Event): boolean { |
| 107 | + const exceptionEntry = event.entries?.find( |
| 108 | + (entry): entry is EntryException => entry.type === EntryType.EXCEPTION |
| 109 | + ); |
| 110 | + if (!exceptionEntry) { |
| 111 | + return false; |
| 112 | + } |
| 113 | + return ( |
| 114 | + exceptionEntry.data.values?.some( |
| 115 | + (value: ExceptionValue) => value.mechanism?.type === 'signalhandler' |
| 116 | + ) ?? false |
| 117 | + ); |
| 118 | +} |
| 119 | + |
| 120 | +function getSdkConfig(event: Event): SdkConfig | undefined { |
| 121 | + const sdkName = event.sdk?.name; |
| 122 | + if (!sdkName) { |
| 123 | + return undefined; |
| 124 | + } |
| 125 | + return SDK_CONFIGS[sdkName]; |
| 126 | +} |
| 127 | + |
| 128 | +export function shouldShowTombstonesBanner(event: Event): boolean { |
| 129 | + return getSdkConfig(event) !== undefined && hasSignalHandlerMechanism(event); |
| 130 | +} |
| 131 | + |
| 132 | +interface Props { |
| 133 | + event: Event; |
| 134 | + projectId: string; |
| 135 | +} |
| 136 | + |
| 137 | +export function AndroidNativeTombstonesBanner({event, projectId}: Props) { |
| 138 | + const organization = useOrganization(); |
| 139 | + const sdkConfig = getSdkConfig(event); |
| 140 | + const [codeTab, setCodeTab] = useState(sdkConfig?.defaultTab ?? 'manifest'); |
| 141 | + |
| 142 | + const {isLoading, isError, isPromptDismissed, dismissPrompt, snoozePrompt} = usePrompt({ |
| 143 | + feature: 'issue_android_tombstones_onboarding', |
| 144 | + organization, |
| 145 | + projectId, |
| 146 | + daysToSnooze: 7, |
| 147 | + }); |
| 148 | + |
| 149 | + if (isLoading || isError || isPromptDismissed || !sdkConfig) { |
| 150 | + return null; |
| 151 | + } |
| 152 | + |
| 153 | + const activeTab = |
| 154 | + sdkConfig.tabs.find(tab => tab.value === codeTab) ?? sdkConfig.tabs[0]!; |
| 155 | + |
| 156 | + return ( |
| 157 | + <BannerWrapper> |
| 158 | + <Flex direction="column" gap="md"> |
| 159 | + <Heading as="h4">{t('Enable Tombstone Collection')}</Heading> |
| 160 | + <Text as="p" style={{maxWidth: 460}}> |
| 161 | + {t( |
| 162 | + '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.' |
| 163 | + )} |
| 164 | + </Text> |
| 165 | + <CodeBlock |
| 166 | + tabs={sdkConfig.tabs.map(tab => ({label: tab.label, value: tab.value}))} |
| 167 | + selectedTab={codeTab} |
| 168 | + onTabClick={setCodeTab} |
| 169 | + language={activeTab.language} |
| 170 | + > |
| 171 | + {activeTab.code} |
| 172 | + </CodeBlock> |
| 173 | + <LinkButton |
| 174 | + style={{alignSelf: 'flex-start'}} |
| 175 | + href={TOMBSTONES_DOCS_URL} |
| 176 | + external |
| 177 | + priority="primary" |
| 178 | + size="sm" |
| 179 | + analyticsEventName="Clicked Android Tombstones Onboarding CTA" |
| 180 | + analyticsEventKey="issue-details.android-tombstones-onboarding-cta-clicked" |
| 181 | + analyticsParams={{ |
| 182 | + organization, |
| 183 | + sdk_name: event.sdk?.name ?? '', |
| 184 | + }} |
| 185 | + > |
| 186 | + {t('Learn More')} |
| 187 | + </LinkButton> |
| 188 | + </Flex> |
| 189 | + <CloseDropdownMenu |
| 190 | + position="bottom-end" |
| 191 | + triggerProps={{ |
| 192 | + showChevron: false, |
| 193 | + priority: 'transparent', |
| 194 | + icon: <IconClose variant="muted" />, |
| 195 | + }} |
| 196 | + size="xs" |
| 197 | + items={[ |
| 198 | + { |
| 199 | + key: 'dismiss', |
| 200 | + label: t('Dismiss'), |
| 201 | + onAction: () => { |
| 202 | + dismissPrompt(); |
| 203 | + trackAnalytics('issue-details.android-tombstones-cta-dismiss', { |
| 204 | + organization, |
| 205 | + type: 'dismiss', |
| 206 | + }); |
| 207 | + }, |
| 208 | + }, |
| 209 | + { |
| 210 | + key: 'snooze', |
| 211 | + label: t('Snooze'), |
| 212 | + onAction: () => { |
| 213 | + snoozePrompt(); |
| 214 | + trackAnalytics('issue-details.android-tombstones-cta-dismiss', { |
| 215 | + organization, |
| 216 | + type: 'snooze', |
| 217 | + }); |
| 218 | + }, |
| 219 | + }, |
| 220 | + ]} |
| 221 | + /> |
| 222 | + </BannerWrapper> |
| 223 | + ); |
| 224 | +} |
| 225 | + |
| 226 | +const BannerWrapper = styled('div')` |
| 227 | + position: relative; |
| 228 | + border: 1px solid ${p => p.theme.tokens.border.primary}; |
| 229 | + border-radius: ${p => p.theme.radius.md}; |
| 230 | + padding: ${p => p.theme.space.xl}; |
| 231 | + margin: ${p => p.theme.space.md} 0; |
| 232 | + background: linear-gradient( |
| 233 | + 90deg, |
| 234 | + color-mix(in srgb, ${p => p.theme.tokens.background.secondary} 0%, transparent) 0%, |
| 235 | + ${p => p.theme.tokens.background.secondary} 70%, |
| 236 | + ${p => p.theme.tokens.background.secondary} 100% |
| 237 | + ); |
| 238 | +`; |
| 239 | + |
| 240 | +const CloseDropdownMenu = styled(DropdownMenu)` |
| 241 | + position: absolute; |
| 242 | + display: block; |
| 243 | + top: ${p => p.theme.space.md}; |
| 244 | + right: ${p => p.theme.space.md}; |
| 245 | + cursor: pointer; |
| 246 | + z-index: 1; |
| 247 | +`; |
0 commit comments