Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import {useState} from 'react';
import styled from '@emotion/styled';

import {LinkButton} from '@sentry/scraps/button';
import {CodeBlock} from '@sentry/scraps/code';

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} from 'sentry/types/event';
import {EntryType} from 'sentry/types/event';
import {trackAnalytics} from 'sentry/utils/analytics';
import {useOrganization} from 'sentry/utils/useOrganization';

const ANDROID_NATIVE_SDK_PREFIX = 'sentry.native.android';
const TOMBSTONES_DOCS_URL =
'https://docs.sentry.io/platforms/android/configuration/tombstones/';

const CODE_SNIPPETS: Record<string, string> = {
manifest: `<application>
<meta-data
android:name="io.sentry.tombstone.enable"
android:value="true" />
</application>`,
kotlin: `SentryAndroid.init(context) { options ->
options.isReportHistoricalTombstones = true
}`,
java: `SentryAndroid.init(context, options -> {
options.setReportHistoricalTombstones(true);
});`,
};

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 => value.mechanism?.type === 'signalhandler'

Check failure on line 43 in static/app/components/events/interfaces/crashContent/exception/androidNativeTombstonesBanner.tsx

View workflow job for this annotation

GitHub Actions / @typescript/native-preview

Parameter 'value' implicitly has an 'any' type.

Check failure on line 43 in static/app/components/events/interfaces/crashContent/exception/androidNativeTombstonesBanner.tsx

View workflow job for this annotation

GitHub Actions / typescript

Parameter 'value' implicitly has an 'any' type.
) ?? false
);
}

function isAndroidNativeSdk(event: Event): boolean {
return event.sdk?.name?.startsWith(ANDROID_NATIVE_SDK_PREFIX) ?? false;
}

export function shouldShowTombstonesBanner(event: Event): boolean {
return isAndroidNativeSdk(event) && hasSignalHandlerMechanism(event);
}

interface Props {
event: Event;
projectId: string;
}

export function AndroidNativeTombstonesBanner({event, projectId}: Props) {
const organization = useOrganization();
const [codeTab, setCodeTab] = useState('manifest');

const {isLoading, isError, isPromptDismissed, dismissPrompt, snoozePrompt} = usePrompt({
feature: 'issue_android_tombstones_onboarding',
organization,
projectId,
daysToSnooze: 7,
});

if (isLoading || isError || isPromptDismissed) {
return null;
}

return (
<BannerWrapper>
<div>
<BannerTitle>{t('Enable Tombstone Collection')}</BannerTitle>
<BannerDescription>
{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.'
)}
</BannerDescription>
<CodeBlock

Check failure on line 85 in static/app/components/events/interfaces/crashContent/exception/androidNativeTombstonesBanner.tsx

View workflow job for this annotation

GitHub Actions / @typescript/native-preview

This JSX tag's 'children' prop expects type 'string' which requires multiple children, but only a single child was provided.

Check failure on line 85 in static/app/components/events/interfaces/crashContent/exception/androidNativeTombstonesBanner.tsx

View workflow job for this annotation

GitHub Actions / typescript

This JSX tag's 'children' prop expects type 'string' which requires multiple children, but only a single child was provided.
tabs={[
{label: 'AndroidManifest.xml', value: 'manifest'},
{label: 'Kotlin', value: 'kotlin'},
{label: 'Java', value: 'java'},
]}
selectedTab={codeTab}
onTabClick={setCodeTab}
language={codeTab === 'manifest' ? 'xml' : codeTab}
>
{CODE_SNIPPETS[codeTab]}
</CodeBlock>
<LinkButton
style={{marginTop: '12px'}}
href={TOMBSTONES_DOCS_URL}
external
priority="primary"
size="sm"
analyticsEventName="Clicked Android Tombstones Onboarding CTA"
analyticsEventKey="issue-details.android-tombstones-onboarding-cta-clicked"
analyticsParams={{
organization,
sdk_name: event.sdk?.name ?? '',
}}
>
{t('Learn More')}
</LinkButton>
</div>
<CloseDropdownMenu
position="bottom-end"
triggerProps={{
showChevron: false,
priority: 'transparent',
icon: <IconClose variant="muted" />,
}}
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',
});
},
},
]}
/>
</BannerWrapper>
);
}

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 BannerTitle = styled('div')`
font-size: ${p => p.theme.font.size.xl};
margin-bottom: ${p => p.theme.space.md};
font-weight: ${p => p.theme.font.weight.sans.medium};
`;

const BannerDescription = styled('div')`
margin-bottom: ${p => p.theme.space.lg};
max-width: 460px;
`;

const CloseDropdownMenu = styled(DropdownMenu)`
position: absolute;
display: block;
top: ${p => p.theme.space.md};
right: ${p => p.theme.space.md};
color: ${p => p.theme.colors.white};
cursor: pointer;
z-index: 1;
`;
3 changes: 3 additions & 0 deletions static/app/utils/analytics/issueAnalyticsEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -388,6 +389,8 @@ export const issueEventMap: Record<IssueEventKey, string | null> = {
'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':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -260,6 +265,14 @@ export function EventDetailsContent({
display: block !important;
`}
>
{shouldShowTombstonesBanner(event) && !isSampleError && (
<ErrorBoundary mini>
<AndroidNativeTombstonesBanner
event={event}
projectId={group?.project.id ?? event.projectID ?? ''}
/>
</ErrorBoundary>
)}
{defined(eventEntries[EntryType.EXCEPTION]) && (
<EntryErrorBoundary type={EntryType.EXCEPTION}>
{shouldUseNewStackTrace ? (
Expand Down
Loading