solutionsHubDrawer.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import starImage from 'sentry-images/spot/banner-star.svg';
  4. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  5. import FeatureBadge from 'sentry/components/badge/featureBadge';
  6. import {Breadcrumbs as NavigationBreadcrumbs} from 'sentry/components/breadcrumbs';
  7. import {Button} from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import AutofixFeedback from 'sentry/components/events/autofix/autofixFeedback';
  10. import {AutofixSetupContent} from 'sentry/components/events/autofix/autofixSetupModal';
  11. import {AutofixSteps} from 'sentry/components/events/autofix/autofixSteps';
  12. import {useAiAutofix} from 'sentry/components/events/autofix/useAutofix';
  13. import {useAutofixSetup} from 'sentry/components/events/autofix/useAutofixSetup';
  14. import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components';
  15. import {GroupSummaryBody, useGroupSummary} from 'sentry/components/group/groupSummary';
  16. import Input from 'sentry/components/input';
  17. import {IconDocs, IconSeer} from 'sentry/icons';
  18. import {t, tct} from 'sentry/locale';
  19. import {space} from 'sentry/styles/space';
  20. import {EntryType, type Event} from 'sentry/types/event';
  21. import type {Group} from 'sentry/types/group';
  22. import type {Organization} from 'sentry/types/organization';
  23. import type {Project} from 'sentry/types/project';
  24. import {getShortEventId} from 'sentry/utils/events';
  25. import {
  26. getConfigForIssueType,
  27. shouldShowCustomErrorResourceConfig,
  28. } from 'sentry/utils/issueTypeConfig';
  29. import {getRegionDataFromOrganization} from 'sentry/utils/regions';
  30. import useRouteAnalyticsParams from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams';
  31. import useOrganization from 'sentry/utils/useOrganization';
  32. import {MIN_NAV_HEIGHT} from 'sentry/views/issueDetails/streamline/eventTitle';
  33. import Resources from 'sentry/views/issueDetails/streamline/resources';
  34. import {useIsSampleEvent} from 'sentry/views/issueDetails/utils';
  35. interface AutofixStartBoxProps {
  36. groupId: string;
  37. onSend: (message: string) => void;
  38. }
  39. function AutofixStartBox({onSend, groupId}: AutofixStartBoxProps) {
  40. const [message, setMessage] = useState('');
  41. const send = () => {
  42. onSend(message);
  43. };
  44. return (
  45. <StartBox>
  46. <StarTrail>
  47. {[...Array(8)].map((_, i) => (
  48. <TrailStar
  49. key={i}
  50. src={starImage}
  51. index={i}
  52. size={14 + i * 2}
  53. offset={(i % 2) * 50 - 15}
  54. />
  55. ))}
  56. </StarTrail>
  57. <ContentContainer>
  58. <HeaderText>Autofix</HeaderText>
  59. <p>Work together with Autofix to find the root cause and fix the issue.</p>
  60. <Row>
  61. <Input
  62. type="text"
  63. value={message}
  64. onChange={e => setMessage(e.target.value)}
  65. placeholder={'(Optional) Share helpful context here...'}
  66. />
  67. <ButtonWithStars>
  68. <StarLarge1 src={starImage} />
  69. <StarLarge2 src={starImage} />
  70. <StarLarge3 src={starImage} />
  71. <Button
  72. priority="primary"
  73. onClick={send}
  74. analyticsEventKey={
  75. message
  76. ? 'autofix.give_instructions_clicked'
  77. : 'autofix.start_fix_clicked'
  78. }
  79. analyticsEventName={
  80. message
  81. ? 'Autofix: Give Instructions Clicked'
  82. : 'Autofix: Start Fix Clicked'
  83. }
  84. analyticsParams={{group_id: groupId}}
  85. >
  86. {message ? 'Start' : 'Start Autofix'}
  87. </Button>
  88. </ButtonWithStars>
  89. </Row>
  90. </ContentContainer>
  91. </StartBox>
  92. );
  93. }
  94. const shouldDisplayAiAutofixForOrganization = (organization: Organization) => {
  95. return (
  96. ((organization.features.includes('autofix') &&
  97. organization.features.includes('issue-details-autofix-ui')) ||
  98. organization.genAIConsent) &&
  99. !organization.hideAiFeatures &&
  100. getRegionDataFromOrganization(organization)?.name !== 'de'
  101. );
  102. };
  103. // Autofix requires the event to have stack trace frames in order to work correctly.
  104. function hasStacktraceWithFrames(event: Event) {
  105. for (const entry of event.entries) {
  106. if (entry.type === EntryType.EXCEPTION) {
  107. if (entry.data.values?.some(value => value.stacktrace?.frames?.length)) {
  108. return true;
  109. }
  110. }
  111. if (entry.type === EntryType.THREADS) {
  112. if (entry.data.values?.some(thread => thread.stacktrace?.frames?.length)) {
  113. return true;
  114. }
  115. }
  116. }
  117. return false;
  118. }
  119. interface SolutionsHubDrawerProps {
  120. event: Event;
  121. group: Group;
  122. project: Project;
  123. }
  124. export function SolutionsHubDrawer({group, project, event}: SolutionsHubDrawerProps) {
  125. const {autofixData, triggerAutofix, reset} = useAiAutofix(group, event);
  126. const {data: summaryData, isError} = useGroupSummary(group.id, group.issueCategory);
  127. const {
  128. data: setupData,
  129. isPending: isSetupLoading,
  130. refetch: refetchSetup,
  131. } = useAutofixSetup({
  132. groupId: group.id,
  133. });
  134. useRouteAnalyticsParams({
  135. autofix_status: autofixData?.status ?? 'none',
  136. });
  137. const config = getConfigForIssueType(group, project);
  138. const isSetupComplete = setupData?.integration.ok && setupData?.genAIConsent.ok;
  139. const hasSummary = summaryData && !isError && setupData?.genAIConsent.ok;
  140. const organization = useOrganization();
  141. const isSampleError = useIsSampleEvent();
  142. const displayAiAutofix =
  143. shouldDisplayAiAutofixForOrganization(organization) &&
  144. config.autofix &&
  145. !shouldShowCustomErrorResourceConfig(group, project) &&
  146. hasStacktraceWithFrames(event) &&
  147. !isSampleError;
  148. return (
  149. <SolutionsDrawerContainer>
  150. <SolutionsDrawerHeader>
  151. <NavigationCrumbs
  152. crumbs={[
  153. {
  154. label: (
  155. <CrumbContainer>
  156. <ProjectAvatar project={project} />
  157. <ShortId>{group.shortId}</ShortId>
  158. </CrumbContainer>
  159. ),
  160. },
  161. {label: getShortEventId(event.id)},
  162. {label: t('Solutions Hub')},
  163. ]}
  164. />
  165. </SolutionsDrawerHeader>
  166. <SolutionsDrawerNavigator>
  167. <Header>{t('Solutions Hub')}</Header>
  168. {autofixData && (
  169. <ButtonBar gap={1}>
  170. <AutofixFeedback />
  171. <Button
  172. size="xs"
  173. onClick={reset}
  174. title={
  175. autofixData.created_at
  176. ? `Last run at ${autofixData.created_at.split('T')[0]}`
  177. : null
  178. }
  179. >
  180. {t('Start Over')}
  181. </Button>
  182. </ButtonBar>
  183. )}
  184. </SolutionsDrawerNavigator>
  185. <SolutionsDrawerBody>
  186. {config.resources && (
  187. <ResourcesContainer>
  188. <ResourcesHeader>
  189. <IconDocs size="md" />
  190. {t('Resources')}
  191. </ResourcesHeader>
  192. <ResourcesBody>
  193. <Resources
  194. eventPlatform={event?.platform}
  195. group={group}
  196. configResources={config.resources}
  197. />
  198. </ResourcesBody>
  199. </ResourcesContainer>
  200. )}
  201. <HeaderText>
  202. <IconSeer size="lg" />
  203. {t('Sentry AI')}
  204. <StyledFeatureBadge
  205. type="beta"
  206. title={tct(
  207. 'This feature is in beta. Try it out and let us know your feedback at [email:autofix@sentry.io].',
  208. {
  209. email: <a href="mailto:autofix@sentry.io" />,
  210. }
  211. )}
  212. />
  213. </HeaderText>
  214. {hasSummary && (
  215. <StyledCard>
  216. <GroupSummaryBody data={summaryData} isError={isError} />
  217. </StyledCard>
  218. )}
  219. {displayAiAutofix && (
  220. <Fragment>
  221. {!isSetupLoading && !isSetupComplete ? (
  222. <SetupContainer>
  223. <AutofixSetupContent
  224. projectId={project.id}
  225. groupId={group.id}
  226. onComplete={refetchSetup}
  227. />
  228. </SetupContainer>
  229. ) : !autofixData ? (
  230. <AutofixStartBox onSend={triggerAutofix} groupId={group.id} />
  231. ) : (
  232. <AutofixSteps
  233. data={autofixData}
  234. groupId={group.id}
  235. runId={autofixData.run_id}
  236. onRetry={reset}
  237. />
  238. )}
  239. </Fragment>
  240. )}
  241. </SolutionsDrawerBody>
  242. </SolutionsDrawerContainer>
  243. );
  244. }
  245. const ResourcesContainer = styled('div')``;
  246. const ResourcesBody = styled('div')`
  247. padding: 0 ${space(2)} ${space(2)} ${space(2)};
  248. border-bottom: 1px solid ${p => p.theme.border};
  249. margin-bottom: ${space(2)};
  250. `;
  251. const Row = styled('div')`
  252. display: flex;
  253. gap: ${space(1)};
  254. `;
  255. const StartBox = styled('div')`
  256. padding: ${space(2)};
  257. position: absolute;
  258. bottom: ${space(2)};
  259. left: ${space(2)};
  260. right: ${space(2)};
  261. `;
  262. const ContentContainer = styled('div')`
  263. position: relative;
  264. z-index: 1;
  265. margin-top: 80px;
  266. `;
  267. const SolutionsDrawerContainer = styled('div')`
  268. height: 100%;
  269. display: grid;
  270. grid-template-rows: auto auto 1fr;
  271. `;
  272. const SolutionsDrawerHeader = styled(DrawerHeader)`
  273. position: unset;
  274. max-height: ${MIN_NAV_HEIGHT}px;
  275. box-shadow: none;
  276. border-bottom: 1px solid ${p => p.theme.border};
  277. `;
  278. const SolutionsDrawerNavigator = styled('div')`
  279. display: grid;
  280. grid-template-columns: 1fr auto;
  281. align-items: center;
  282. column-gap: ${space(1)};
  283. padding: ${space(0.75)} 24px;
  284. background: ${p => p.theme.background};
  285. z-index: 1;
  286. min-height: ${MIN_NAV_HEIGHT}px;
  287. box-shadow: ${p => p.theme.translucentBorder} 0 1px;
  288. `;
  289. const SolutionsDrawerBody = styled(DrawerBody)`
  290. overflow: auto;
  291. overscroll-behavior: contain;
  292. /* Move the scrollbar to the left edge */
  293. scroll-margin: 0 ${space(2)};
  294. direction: rtl;
  295. * {
  296. direction: ltr;
  297. }
  298. `;
  299. const Header = styled('h3')`
  300. display: block;
  301. font-size: ${p => p.theme.fontSizeExtraLarge};
  302. font-weight: ${p => p.theme.fontWeightBold};
  303. margin: 0;
  304. `;
  305. const NavigationCrumbs = styled(NavigationBreadcrumbs)`
  306. margin: 0;
  307. padding: 0;
  308. `;
  309. const CrumbContainer = styled('div')`
  310. display: flex;
  311. gap: ${space(1)};
  312. align-items: center;
  313. `;
  314. const ShortId = styled('div')`
  315. font-family: ${p => p.theme.text.family};
  316. font-size: ${p => p.theme.fontSizeMedium};
  317. line-height: 1;
  318. `;
  319. const ButtonWithStars = styled('div')`
  320. position: relative;
  321. display: flex;
  322. align-items: center;
  323. `;
  324. const StarLarge = styled('img')`
  325. position: absolute;
  326. z-index: 0;
  327. filter: sepia(1) saturate(3) hue-rotate(290deg);
  328. `;
  329. const StarLarge1 = styled(StarLarge)`
  330. left: 45px;
  331. bottom: -15px;
  332. transform: rotate(90deg);
  333. width: 16px;
  334. height: 16px;
  335. `;
  336. const StarLarge2 = styled(StarLarge)`
  337. left: -5px;
  338. top: -15px;
  339. transform: rotate(-30deg);
  340. width: 24px;
  341. height: 24px;
  342. `;
  343. const StarLarge3 = styled(StarLarge)`
  344. right: -25px;
  345. bottom: 0px;
  346. transform: rotate(20deg);
  347. width: 28px;
  348. height: 28px;
  349. `;
  350. const SetupContainer = styled('div')`
  351. padding: ${space(2)};
  352. /* Override some modal-specific styles */
  353. h3 {
  354. font-size: ${p => p.theme.fontSizeLarge};
  355. margin-bottom: ${space(2)};
  356. }
  357. `;
  358. const StyledCard = styled('div')`
  359. background: ${p => p.theme.backgroundElevated};
  360. border-radius: ${p => p.theme.borderRadius};
  361. border: 1px solid ${p => p.theme.border};
  362. overflow: hidden;
  363. box-shadow: ${p => p.theme.dropShadowMedium};
  364. padding-bottom: ${space(1)};
  365. `;
  366. const HeaderText = styled('div')`
  367. font-weight: bold;
  368. font-size: ${p => p.theme.fontSizeLarge};
  369. display: flex;
  370. align-items: center;
  371. gap: ${space(0.5)};
  372. padding-bottom: ${space(2)};
  373. `;
  374. const StyledFeatureBadge = styled(FeatureBadge)`
  375. margin-left: ${space(0.25)};
  376. padding-bottom: 3px;
  377. `;
  378. const ResourcesHeader = styled(HeaderText)`
  379. gap: ${space(1)};
  380. `;
  381. const StarTrail = styled('div')`
  382. height: 400px;
  383. width: 100%;
  384. display: flex;
  385. justify-content: center;
  386. position: absolute;
  387. bottom: 7rem;
  388. left: 0;
  389. right: 0;
  390. z-index: -1;
  391. pointer-events: none;
  392. `;
  393. const TrailStar = styled('img')<{index: number; offset: number; size: number}>`
  394. position: absolute;
  395. width: ${p => p.size}px;
  396. height: ${p => p.size}px;
  397. top: ${p => p.index * 50}px;
  398. transform: translateX(${p => p.offset}px) rotate(${p => p.index * 40}deg);
  399. opacity: ${p => Math.min(1, 0.2 + p.index * 0.1)};
  400. filter: sepia(1) saturate(3) hue-rotate(290deg);
  401. `;