Skip to content

Commit 88d84e3

Browse files
romtsnclaude
andcommitted
feat(issue-details): Add android native tombstones onboarding banner
Show an onboarding banner on the issue details page when a native crash was captured via the Android NDK integration with only a signalhandler mechanism. The banner includes tabbed code snippets (AndroidManifest.xml, Kotlin, Java) showing how to enable tombstone collection, with dismiss and snooze support via the prompts-activity API. Depends on #112477 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0654c62 commit 88d84e3

File tree

3 files changed

+204
-0
lines changed

3 files changed

+204
-0
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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+
`;

static/app/utils/analytics/issueAnalyticsEvents.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type IssueEventParameters = {
7272
'integrations.integration_reinstall_clicked': {
7373
provider: string;
7474
};
75+
'issue-details.android-tombstones-cta-dismiss': {type: string};
7576
'issue-details.replay-cta-dismiss': {type: string};
7677
'issue.engaged_view': {
7778
group_id: number;
@@ -400,6 +401,8 @@ export const issueEventMap: Record<IssueEventKey, string | null> = {
400401
'issue_details.event_dropdown_option_selected':
401402
'Issue Details: Event Dropdown Option Selected',
402403
'issue_details.header_view_replay_clicked': 'Issue Details: Header View Replay Clicked',
404+
'issue-details.android-tombstones-cta-dismiss':
405+
'Issue Details Android Tombstones CTA Dismissed',
403406
'issue-details.replay-cta-dismiss': 'Issue Details Replay CTA Dismissed',
404407
'issue_group_details.anr_root_cause_detected': 'Detected ANR Root Cause',
405408
'issue_details.copy_issue_details_as_markdown':

static/app/views/issueDetails/groupEventDetails/groupEventDetailsContent.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import {EventFeatureFlagSection} from 'sentry/components/events/featureFlags/eve
3030
import {EventGroupingInfoSection} from 'sentry/components/events/groupingInfo/groupingInfoSection';
3131
import {HighlightsDataSection} from 'sentry/components/events/highlights/highlightsDataSection';
3232
import {HighlightsIconSummary} from 'sentry/components/events/highlights/highlightsIconSummary';
33+
import {
34+
AndroidNativeTombstonesBanner,
35+
shouldShowTombstonesBanner,
36+
} from 'sentry/components/events/interfaces/crashContent/exception/androidNativeTombstonesBanner';
3337
import {Csp} from 'sentry/components/events/interfaces/csp';
3438
import {DebugMeta} from 'sentry/components/events/interfaces/debugMeta';
3539
import {Exception} from 'sentry/components/events/interfaces/exception';
@@ -79,6 +83,7 @@ import {InstrumentationFixSection} from 'sentry/views/issueDetails/streamline/in
7983
import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection';
8084
import {MetricDetectorTriggeredSection} from 'sentry/views/issueDetails/streamline/sidebar/metricDetectorTriggeredSection';
8185
import {SizeAnalysisTriggeredSection} from 'sentry/views/issueDetails/streamline/sidebar/sizeAnalysisTriggeredSection';
86+
import {useIsSampleEvent} from 'sentry/views/issueDetails/utils';
8287
import {DEFAULT_TRACE_VIEW_PREFERENCES} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
8388
import {TraceStateProvider} from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider';
8489

@@ -118,6 +123,7 @@ export function EventDetailsContent({
118123
: null;
119124
const isMetricKitHang = hangProfileData !== null;
120125
const groupingCurrentLevel = group?.metadata?.current_level;
126+
const isSampleError = useIsSampleEvent();
121127

122128
useCopyIssueDetails(group, event);
123129

@@ -184,6 +190,14 @@ export function EventDetailsContent({
184190
display: block !important;
185191
`}
186192
>
193+
{shouldShowTombstonesBanner(event) && !isSampleError && (
194+
<ErrorBoundary mini>
195+
<AndroidNativeTombstonesBanner
196+
event={event}
197+
projectId={group?.project.id ?? event.projectID ?? ''}
198+
/>
199+
</ErrorBoundary>
200+
)}
187201
{defined(eventEntries[EntryType.EXCEPTION]) && (
188202
<EntryErrorBoundary type={EntryType.EXCEPTION}>
189203
{shouldUseNewStackTrace ? (

0 commit comments

Comments
 (0)