Browse Source

feat(open-ai): Add AI suggested fix to the UI - (#46384)

Priscila Oliveira 1 year ago
parent
commit
ab56e43ef5

+ 5 - 1
static/app/components/featureBadge.tsx

@@ -11,7 +11,7 @@ import {t} from 'sentry/locale';
 import space, {ValidSize} from 'sentry/styles/space';
 
 type BadgeProps = {
-  type: 'alpha' | 'beta' | 'new';
+  type: 'alpha' | 'beta' | 'new' | 'experimental';
   expiresAt?: Date;
   noTooltip?: boolean;
   title?: string;
@@ -24,12 +24,16 @@ const defaultTitles = {
   alpha: t('This feature is internal and available for QA purposes'),
   beta: t('This feature is available for early adopters and may change'),
   new: t('This feature is new! Try it out and let us know what you think'),
+  experimental: t(
+    'This feature is experimental! Try it out and let us know what you think. No promises!'
+  ),
 };
 
 const labels = {
   alpha: t('alpha'),
   beta: t('beta'),
   new: t('new'),
+  experimental: t('experimental'),
 };
 
 function BaseFeatureBadge({

+ 1 - 1
static/app/components/featureFeedback/feedbackModal.tsx

@@ -30,7 +30,7 @@ import {useLocation} from 'sentry/utils/useLocation';
 import useMedia from 'sentry/utils/useMedia';
 import useProjects from 'sentry/utils/useProjects';
 
-const feedbackClient = new BrowserClient({
+export const feedbackClient = new BrowserClient({
   // feedback project under Sentry organization
   dsn: 'https://3c5ef4e344a04a0694d187a1272e96de@o1.ingest.sentry.io/6356259',
   transport: makeFetchTransport,

+ 1 - 0
static/app/utils/analytics/workflowAnalyticsEvents.tsx

@@ -71,6 +71,7 @@ export type TeamInsightsEventParameters = {
       | 'shared'
       | 'discarded'
       | 'open_in_discover'
+      | 'open_ai_suggested_fix'
       | 'assign'
       | ResolutionStatus;
     assigned_suggestion_reason?: string;

+ 5 - 0
static/app/utils/theme.tsx

@@ -483,6 +483,11 @@ const generateBadgeTheme = (colors: BaseColors) => ({
     indicatorColor: colors.green300,
     color: colors.white,
   },
+  experimental: {
+    background: `linear-gradient(90deg, ${colors.blue300}, ${colors.green300})`,
+    indicatorColor: colors.green300,
+    color: colors.white,
+  },
   review: {
     background: colors.purple300,
     indicatorColor: colors.purple300,

+ 31 - 0
static/app/views/issueDetails/actions/index.tsx

@@ -20,6 +20,8 @@ import GuideAnchor from 'sentry/components/assistant/guideAnchor';
 import {Button} from 'sentry/components/button';
 import {DropdownMenu} from 'sentry/components/dropdownMenu';
 import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
+import FeatureBadge from 'sentry/components/featureBadge';
+import {Tooltip} from 'sentry/components/tooltip';
 import {
   IconCheckmark,
   IconEllipsis,
@@ -51,6 +53,8 @@ import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
 import withApi from 'sentry/utils/withApi';
 import {normalizeUrl} from 'sentry/utils/withDomainRequired';
 import withOrganization from 'sentry/utils/withOrganization';
+import {OpenAIFixSuggestionButton} from 'sentry/views/issueDetails/openAIFixSuggestion/openAIFixSuggestionButton';
+import {experimentalFeatureTooltipDesc} from 'sentry/views/issueDetails/openAIFixSuggestion/utils';
 
 import ShareIssueModal from './shareModal';
 import SubscribeAction from './subscribeAction';
@@ -107,6 +111,7 @@ class Actions extends Component<Props> {
       | 'mark_reviewed'
       | 'discarded'
       | 'open_in_discover'
+      | 'open_ai_suggested_fix'
       | ResolutionStatus
   ) {
     const {group, project, organization, query = {}} = this.props;
@@ -402,6 +407,21 @@ class Actions extends Component<Props> {
               to: disabled ? '' : this.getDiscoverUrl(),
               onAction: () => this.trackIssueAction('open_in_discover'),
             },
+            {
+              key: 'suggested-fix',
+              className: 'hidden-sm hidden-md hidden-lg',
+              label: (
+                <Tooltip
+                  title={experimentalFeatureTooltipDesc}
+                  containerDisplayMode="inline-flex"
+                >
+                  {t('Suggested Fix')}
+                  <FeatureBadge type="experimental" noTooltip />
+                </Tooltip>
+              ),
+              onAction: () => this.trackIssueAction('open_ai_suggested_fix'),
+              hidden: !orgFeatures.has('open-ai-suggestion'),
+            },
             {
               key: group.isSubscribed ? 'unsubscribe' : 'subscribe',
               className: 'hidden-sm hidden-md hidden-lg',
@@ -482,6 +502,17 @@ class Actions extends Component<Props> {
             <GuideAnchor target="open_in_discover">{t('Open in Discover')}</GuideAnchor>
           </ActionButton>
         </Feature>
+        <Feature features={['open-ai-suggestion']} organization={organization}>
+          <GuideAnchor target="suggested-fix" position="bottom" offset={20}>
+            <OpenAIFixSuggestionButton
+              className="hidden-xs"
+              size="sm"
+              disabled={disabled}
+              groupId={group.id}
+              onClick={() => this.trackIssueAction('open_ai_suggested_fix')}
+            />
+          </GuideAnchor>
+        </Feature>
         {isResolved || isIgnored ? (
           <ActionButton
             priority="primary"

+ 5 - 0
static/app/views/issueDetails/groupEventDetails/groupEventDetails.spec.tsx

@@ -240,6 +240,11 @@ const mockGroupApis = (
     url: '/organizations/org-slug/users/',
     body: [],
   });
+
+  MockApiClient.addMockResponse({
+    url: `/customers/org-slug/policies/`,
+    body: {},
+  });
 };
 
 describe('groupEventDetails', () => {

+ 2 - 0
static/app/views/issueDetails/groupEventHeader.tsx

@@ -8,6 +8,7 @@ import {Event} from 'sentry/types/event';
 import {useLocation} from 'sentry/utils/useLocation';
 import useOrganization from 'sentry/utils/useOrganization';
 import {GroupEventCarousel} from 'sentry/views/issueDetails/groupEventCarousel';
+import {OpenAIFixSuggestionPanel} from 'sentry/views/issueDetails/openAIFixSuggestion/openAIFixSuggestionPanel';
 
 import QuickTrace from './quickTrace';
 
@@ -25,6 +26,7 @@ const GroupEventHeader = ({event, group, project}: GroupEventHeaderProps) => {
   return (
     <DataSection>
       <GroupEventCarousel group={group} event={event} projectSlug={project.slug} />
+      <OpenAIFixSuggestionPanel projectSlug={project.slug} eventID={event.eventID} />
       <QuickTrace
         event={event}
         group={group}

+ 83 - 0
static/app/views/issueDetails/openAIFixSuggestion/openAIFixSuggestionButton.tsx

@@ -0,0 +1,83 @@
+import {useCallback} from 'react';
+
+import ActionButton from 'sentry/components/actions/button';
+import Confirm from 'sentry/components/confirm';
+import FeatureBadge from 'sentry/components/featureBadge';
+import {t} from 'sentry/locale';
+import {Group} from 'sentry/types';
+import useOrganization from 'sentry/utils/useOrganization';
+import useRouter from 'sentry/utils/useRouter';
+import {useCustomerPolicies} from 'sentry/views/issueDetails/openAIFixSuggestion/useCustomerPolicies';
+import {useOpenAISuggestionLocalStorage} from 'sentry/views/issueDetails/openAIFixSuggestion/useOpenAISuggestionLocalStorage';
+import {experimentalFeatureTooltipDesc} from 'sentry/views/issueDetails/openAIFixSuggestion/utils';
+
+type Props = {
+  groupId: Group['id'];
+  onClick: () => void;
+  className?: string;
+  disabled?: boolean;
+  size?: 'xs' | 'sm';
+};
+
+export function OpenAIFixSuggestionButton({
+  className,
+  disabled,
+  size,
+  onClick,
+  groupId,
+}: Props) {
+  const organization = useOrganization();
+  const router = useRouter();
+
+  const [agreedForwardDataToOpenAI, setAgreedForwardDataToOpenAI] =
+    useOpenAISuggestionLocalStorage();
+  const {hasSignedDPA} = useCustomerPolicies();
+
+  const handleShowAISuggestion = useCallback(() => {
+    router.push({
+      pathname: `/issues/${groupId}/`,
+      query: {...router.location.query, showSuggestedFix: true},
+    });
+  }, [router, groupId]);
+
+  const handleDataForwardToOpenAIAgreement = useCallback(() => {
+    setAgreedForwardDataToOpenAI(true);
+    router.push({
+      pathname: `/issues/${groupId}/`,
+      query: {...router.location.query, showSuggestedFix: true},
+    });
+  }, [router, groupId, setAgreedForwardDataToOpenAI]);
+
+  if (!organization.features.includes('open-ai-suggestion')) {
+    return null;
+  }
+
+  const byPassNoGuaranteeModal = hasSignedDPA || agreedForwardDataToOpenAI;
+
+  return (
+    <Confirm
+      bypass={byPassNoGuaranteeModal}
+      priority="primary"
+      message={t(
+        'By using this feature, you agree that OpenAI is a subprocessor and may process the data that you’ve chosen to submit. Sentry makes no guarantees as to the accuracy of the feature’s AI-generated recommendations.'
+      )}
+      disabled={disabled}
+      onConfirm={
+        byPassNoGuaranteeModal
+          ? handleShowAISuggestion
+          : handleDataForwardToOpenAIAgreement
+      }
+    >
+      <ActionButton
+        className={className}
+        disabled={disabled}
+        size={size}
+        onClick={onClick}
+        title={experimentalFeatureTooltipDesc}
+      >
+        {t('Suggested Fix')}
+        <FeatureBadge type="experimental" noTooltip />
+      </ActionButton>
+    </Confirm>
+  );
+}

+ 209 - 0
static/app/views/issueDetails/openAIFixSuggestion/openAIFixSuggestionPanel.tsx

@@ -0,0 +1,209 @@
+import {Fragment, useCallback, useEffect, useState} from 'react';
+import styled from '@emotion/styled';
+
+import {addSuccessMessage} from 'sentry/actionCreators/indicator';
+import {Button} from 'sentry/components/button';
+import FeatureBadge from 'sentry/components/featureBadge';
+import {feedbackClient} from 'sentry/components/featureFeedback/feedbackModal';
+import LoadingError from 'sentry/components/loadingError';
+import LoadingIndicator from 'sentry/components/loadingIndicator';
+import {Panel, PanelBody, PanelFooter, PanelHeader} from 'sentry/components/panels';
+import {IconHappy, IconMeh, IconSad} from 'sentry/icons';
+import {IconChevron} from 'sentry/icons/iconChevron';
+import {t} from 'sentry/locale';
+import ConfigStore from 'sentry/stores/configStore';
+import {space} from 'sentry/styles/space';
+import marked from 'sentry/utils/marked';
+import {useQuery} from 'sentry/utils/queryClient';
+import useOrganization from 'sentry/utils/useOrganization';
+import useRouter from 'sentry/utils/useRouter';
+import {useCustomerPolicies} from 'sentry/views/issueDetails/openAIFixSuggestion/useCustomerPolicies';
+import {useOpenAISuggestionLocalStorage} from 'sentry/views/issueDetails/openAIFixSuggestion/useOpenAISuggestionLocalStorage';
+import {experimentalFeatureTooltipDesc} from 'sentry/views/issueDetails/openAIFixSuggestion/utils';
+
+enum OpenAISatisfactoryLevel {
+  HELPED = 'helped',
+  PARTIALLY_HELPED = 'partially_helped',
+  DID_NOT_HELP = 'did_not_help',
+}
+
+const openAIFeedback = {
+  [OpenAISatisfactoryLevel.HELPED]: t('It helped me solve the issue'),
+  [OpenAISatisfactoryLevel.PARTIALLY_HELPED]: t('It partially helped me solve the issue'),
+  [OpenAISatisfactoryLevel.DID_NOT_HELP]: t('It did not help me solve the issue'),
+};
+
+type Props = {
+  eventID: string;
+  projectSlug: string;
+};
+
+export function OpenAIFixSuggestionPanel({eventID, projectSlug}: Props) {
+  const user = ConfigStore.get('user');
+  const organization = useOrganization();
+  const router = useRouter();
+  const showSuggestedFix = !!router.location.query.showSuggestedFix;
+  const {hasSignedDPA} = useCustomerPolicies();
+  const [agreedForwardDataToOpenAI] = useOpenAISuggestionLocalStorage();
+  const [expandedSuggestedFix, setExpandedSuggestedFix] = useState(showSuggestedFix);
+
+  useEffect(() => {
+    setExpandedSuggestedFix(!!router.location.query.showSuggestedFix);
+  }, [router.location.query.showSuggestedFix]);
+
+  const handleOpenAISuggestionFeedback = useCallback(
+    (openAISatisfactoryLevel: OpenAISatisfactoryLevel) => {
+      feedbackClient.captureEvent({
+        request: {
+          url: window.location.href, // gives the full url (origin + pathname)
+        },
+        tags: {
+          featureName: 'open-ai-suggestion',
+        },
+        user,
+        level: 'info',
+        message: `OpenAI Suggestion Feedback - ${openAIFeedback[openAISatisfactoryLevel]}`,
+      });
+
+      addSuccessMessage('Thank you for your feedback!');
+
+      setExpandedSuggestedFix(false);
+    },
+    [user]
+  );
+
+  const {
+    data,
+    isLoading: dataIsLoading,
+    isError: dataIsError,
+    refetch: dataRefetch,
+  } = useQuery<{suggestion: string}>(
+    [`/projects/${organization.slug}/${projectSlug}/events/${eventID}/ai-fix-suggest/`],
+    {
+      staleTime: Infinity,
+      enabled: (hasSignedDPA || agreedForwardDataToOpenAI) && showSuggestedFix,
+    }
+  );
+
+  if (!organization.features.includes('open-ai-suggestion')) {
+    return null;
+  }
+
+  if (!showSuggestedFix) {
+    return null;
+  }
+
+  return (
+    <FixSuggestionPanel>
+      <FixSuggestionPanelHeader isExpanded={expandedSuggestedFix}>
+        <HeaderDescription>
+          {t('Suggested Fix')}
+          <FeatureBadgeNotUppercase
+            type="experimental"
+            title={experimentalFeatureTooltipDesc}
+          />
+        </HeaderDescription>
+        <Button
+          size="xs"
+          title={t('Toggle Suggested Fix Panel')}
+          aria-label={t('Toggle Suggested Fix Panel')}
+          icon={<IconChevron direction={expandedSuggestedFix ? 'up' : 'down'} />}
+          onClick={() => setExpandedSuggestedFix(!expandedSuggestedFix)}
+        />
+      </FixSuggestionPanelHeader>
+      {expandedSuggestedFix && (
+        <Fragment>
+          <PanelBody withPadding>
+            {dataIsLoading ? (
+              <LoadingIndicator />
+            ) : dataIsError ? (
+              <LoadingErrorWithoutMarginBottom onRetry={dataRefetch} />
+            ) : (
+              <div
+                dangerouslySetInnerHTML={{
+                  __html: marked(data.suggestion, {
+                    gfm: true,
+                    breaks: true,
+                  }),
+                }}
+              />
+            )}
+          </PanelBody>
+          {!dataIsLoading && !dataIsError && (
+            <PanelFooter>
+              <Feedback>
+                <strong>{t('Was this helpful?')}</strong>
+                <div>
+                  <Button
+                    title={openAIFeedback[OpenAISatisfactoryLevel.DID_NOT_HELP]}
+                    aria-label={openAIFeedback[OpenAISatisfactoryLevel.DID_NOT_HELP]}
+                    icon={<IconSad color="red300" />}
+                    size="xs"
+                    borderless
+                    onClick={() =>
+                      handleOpenAISuggestionFeedback(OpenAISatisfactoryLevel.DID_NOT_HELP)
+                    }
+                  />
+                  <Button
+                    title={openAIFeedback[OpenAISatisfactoryLevel.PARTIALLY_HELPED]}
+                    aria-label={openAIFeedback[OpenAISatisfactoryLevel.PARTIALLY_HELPED]}
+                    icon={<IconMeh color="yellow300" />}
+                    size="xs"
+                    borderless
+                    onClick={() =>
+                      handleOpenAISuggestionFeedback(
+                        OpenAISatisfactoryLevel.PARTIALLY_HELPED
+                      )
+                    }
+                  />
+                  <Button
+                    title={openAIFeedback[OpenAISatisfactoryLevel.HELPED]}
+                    aria-label={openAIFeedback[OpenAISatisfactoryLevel.HELPED]}
+                    icon={<IconHappy color="green300" />}
+                    size="xs"
+                    borderless
+                    onClick={() =>
+                      handleOpenAISuggestionFeedback(OpenAISatisfactoryLevel.HELPED)
+                    }
+                  />
+                </div>
+              </Feedback>
+            </PanelFooter>
+          )}
+        </Fragment>
+      )}
+    </FixSuggestionPanel>
+  );
+}
+
+const FixSuggestionPanel = styled(Panel)`
+  margin-top: ${space(1.5)};
+  margin-bottom: 0;
+  overflow: hidden;
+`;
+
+const FixSuggestionPanelHeader = styled(PanelHeader)<{isExpanded: boolean}>`
+  border-bottom: ${p => (p.isExpanded ? 'inherit' : 'none')};
+`;
+
+const HeaderDescription = styled('div')`
+  display: flex;
+  align-items: center;
+`;
+
+const Feedback = styled('div')`
+  padding: ${space(1)} ${space(2)};
+  display: grid;
+  grid-template-columns: 1fr max-content max-content max-content;
+  align-items: center;
+  text-align: right;
+  gap: ${space(1)};
+`;
+
+const LoadingErrorWithoutMarginBottom = styled(LoadingError)`
+  margin-bottom: 0;
+`;
+
+const FeatureBadgeNotUppercase = styled(FeatureBadge)`
+  text-transform: capitalize;
+`;

+ 36 - 0
static/app/views/issueDetails/openAIFixSuggestion/useCustomerPolicies.tsx

@@ -0,0 +1,36 @@
+import {useQuery} from 'sentry/utils/queryClient';
+import useOrganization from 'sentry/utils/useOrganization';
+
+type Policies = Record<
+  string,
+  {
+    active: boolean;
+    consent: null | {
+      createdAt: string;
+      userEmail: string;
+      userName: string;
+    };
+    createdAt: string;
+    hasSignature: boolean;
+    name: string;
+    parent: null;
+    slug: string;
+    standalone: boolean;
+    updatedAt: string;
+    url: string;
+    userEmail: string;
+    userName: string;
+    version: string;
+  }
+>;
+
+export function useCustomerPolicies() {
+  const organization = useOrganization();
+  const {data} = useQuery<Policies>([`/customers/${organization.slug}/policies/`], {
+    staleTime: Infinity,
+  });
+
+  return {
+    hasSignedDPA: !!data?.dpa?.consent,
+  };
+}

Some files were not shown because too many files changed in this diff