userFeedbackEmpty.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. import {useCallback, useEffect} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import emptyStateImg from 'sentry-images/spot/feedback-empty-state.svg';
  5. import {Button} from 'sentry/components/button';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import EmptyStateWarning from 'sentry/components/emptyStateWarning';
  8. import {useFeedbackOnboardingSidebarPanel} from 'sentry/components/feedback/useFeedbackOnboarding';
  9. import OnboardingPanel from 'sentry/components/onboardingPanel';
  10. import {t} from 'sentry/locale';
  11. import {trackAnalytics} from 'sentry/utils/analytics';
  12. import {useLocation} from 'sentry/utils/useLocation';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import useProjects from 'sentry/utils/useProjects';
  15. import useRouter from 'sentry/utils/useRouter';
  16. type Props = {
  17. issueTab?: boolean;
  18. projectIds?: string[];
  19. };
  20. export function UserFeedbackEmpty({projectIds, issueTab = false}: Props) {
  21. const {projects, initiallyLoaded} = useProjects();
  22. const loadingProjects = !initiallyLoaded;
  23. const organization = useOrganization();
  24. const location = useLocation();
  25. const selectedProjects = projectIds?.length
  26. ? projects.filter(({id}) => projectIds.includes(id))
  27. : projects;
  28. const hasAnyFeedback = selectedProjects.some(({hasUserReports}) => hasUserReports);
  29. const hasNewOnboarding = organization.features.includes('user-feedback-onboarding');
  30. const {activateSidebarIssueDetails} = useFeedbackOnboardingSidebarPanel();
  31. const router = useRouter();
  32. const setProjId = useCallback(() => {
  33. router.push({
  34. pathname: location.pathname,
  35. query: {...location.query, project: projectIds?.[0]},
  36. hash: location.hash,
  37. });
  38. }, [location.hash, location.query, location.pathname, projectIds, router]);
  39. useEffect(() => {
  40. if (issueTab) {
  41. setProjId();
  42. }
  43. // eslint-disable-next-line react-hooks/exhaustive-deps
  44. }, []);
  45. useEffect(() => {
  46. window.sentryEmbedCallback = function (embed) {
  47. // Mock the embed's submit xhr to always be successful
  48. // NOTE: this will not have errors if the form is empty
  49. embed.submit = function (_body) {
  50. this._submitInProgress = true;
  51. setTimeout(() => {
  52. this._submitInProgress = false;
  53. this.onSuccess();
  54. }, 500);
  55. };
  56. };
  57. if (hasAnyFeedback === false) {
  58. // send to reload only due to higher event volume
  59. trackAnalytics('user_feedback.viewed', {
  60. organization,
  61. projects: projectIds?.join(',') || '',
  62. });
  63. }
  64. return () => {
  65. window.sentryEmbedCallback = null;
  66. };
  67. }, [hasAnyFeedback, organization, projectIds]);
  68. function trackAnalyticsInternal(
  69. eventKey: 'user_feedback.docs_clicked' | 'user_feedback.dialog_opened'
  70. ) {
  71. trackAnalytics(eventKey, {
  72. organization,
  73. projects: selectedProjects?.join(','),
  74. });
  75. }
  76. // Show no user reports if waiting for projects to load or if there is no feedback
  77. if (loadingProjects || hasAnyFeedback !== false) {
  78. return (
  79. <EmptyStateWarning>
  80. <p>{t('Sorry, no user reports match your filters.')}</p>
  81. </EmptyStateWarning>
  82. );
  83. }
  84. // Show landing page after projects have loaded and it is confirmed no projects have feedback
  85. return (
  86. <OnboardingPanel
  87. data-test-id="user-feedback-empty"
  88. image={<img src={emptyStateImg} />}
  89. >
  90. <h3>{t('What do users think?')}</h3>
  91. <p>
  92. {t(
  93. `You can't read minds. At least we hope not. Ask users for feedback on the impact of their crashes or bugs and you shall receive.`
  94. )}
  95. </p>
  96. <ButtonList gap={1}>
  97. {hasNewOnboarding ? (
  98. <Button
  99. priority="primary"
  100. onClick={activateSidebarIssueDetails}
  101. analyticsEventName="Clicked Feedback Onboarding Setup - Issue Details"
  102. analyticsEventKey="feedback.issue-details-click-onboarding-setup"
  103. >
  104. {t('Set up now')}
  105. </Button>
  106. ) : (
  107. <Button
  108. external
  109. priority="primary"
  110. onClick={() => trackAnalyticsInternal('user_feedback.docs_clicked')}
  111. href="https://docs.sentry.io/product/user-feedback/"
  112. >
  113. {t('Read the docs')}
  114. </Button>
  115. )}
  116. <Button
  117. onClick={() => {
  118. Sentry.showReportDialog({
  119. // should never make it to the Sentry API, but just in case, use throwaway id
  120. eventId: '00000000000000000000000000000000',
  121. });
  122. trackAnalyticsInternal('user_feedback.dialog_opened');
  123. }}
  124. >
  125. {t('See an example')}
  126. </Button>
  127. </ButtonList>
  128. </OnboardingPanel>
  129. );
  130. }
  131. const ButtonList = styled(ButtonBar)`
  132. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  133. `;