userFeedbackEmpty.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import {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 useOrganization from 'sentry/utils/useOrganization';
  13. import useProjects from 'sentry/utils/useProjects';
  14. type Props = {
  15. projectIds?: string[];
  16. };
  17. export function UserFeedbackEmpty({projectIds}: Props) {
  18. const {projects, initiallyLoaded} = useProjects();
  19. const loadingProjects = !initiallyLoaded;
  20. const organization = useOrganization();
  21. const selectedProjects = projectIds?.length
  22. ? projects.filter(({id}) => projectIds.includes(id))
  23. : projects;
  24. const hasAnyFeedback = selectedProjects.some(({hasUserReports}) => hasUserReports);
  25. const hasNewOnboarding = organization.features.includes('user-feedback-onboarding');
  26. const {activateSidebarIssueDetails} = useFeedbackOnboardingSidebarPanel();
  27. useEffect(() => {
  28. window.sentryEmbedCallback = function (embed) {
  29. // Mock the embed's submit xhr to always be successful
  30. // NOTE: this will not have errors if the form is empty
  31. embed.submit = function (_body) {
  32. this._submitInProgress = true;
  33. setTimeout(() => {
  34. this._submitInProgress = false;
  35. this.onSuccess();
  36. }, 500);
  37. };
  38. };
  39. if (hasAnyFeedback === false) {
  40. // send to reload only due to higher event volume
  41. trackAnalytics('user_feedback.viewed', {
  42. organization,
  43. projects: projectIds?.join(',') || '',
  44. });
  45. }
  46. return () => {
  47. window.sentryEmbedCallback = null;
  48. };
  49. }, [hasAnyFeedback, organization, projectIds]);
  50. function trackAnalyticsInternal(
  51. eventKey: 'user_feedback.docs_clicked' | 'user_feedback.dialog_opened'
  52. ) {
  53. trackAnalytics(eventKey, {
  54. organization,
  55. projects: selectedProjects?.join(','),
  56. });
  57. }
  58. // Show no user reports if waiting for projects to load or if there is no feedback
  59. if (loadingProjects || hasAnyFeedback !== false) {
  60. return (
  61. <EmptyStateWarning>
  62. <p>{t('Sorry, no user reports match your filters.')}</p>
  63. </EmptyStateWarning>
  64. );
  65. }
  66. // Show landing page after projects have loaded and it is confirmed no projects have feedback
  67. return (
  68. <OnboardingPanel
  69. data-test-id="user-feedback-empty"
  70. image={<img src={emptyStateImg} />}
  71. >
  72. <h3>{t('What do users think?')}</h3>
  73. <p>
  74. {t(
  75. `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.`
  76. )}
  77. </p>
  78. <ButtonList gap={1}>
  79. {hasNewOnboarding ? (
  80. <Button
  81. priority="primary"
  82. onClick={activateSidebarIssueDetails}
  83. analyticsEventName="Clicked Feedback Onboarding Setup - Issue Details"
  84. analyticsEventKey="feedback.issue-details-click-onboarding-setup"
  85. >
  86. {t('Set up now')}
  87. </Button>
  88. ) : (
  89. <Button
  90. external
  91. priority="primary"
  92. onClick={() => trackAnalyticsInternal('user_feedback.docs_clicked')}
  93. href="https://docs.sentry.io/product/user-feedback/"
  94. >
  95. {t('Read the docs')}
  96. </Button>
  97. )}
  98. <Button
  99. onClick={() => {
  100. Sentry.showReportDialog({
  101. // should never make it to the Sentry API, but just in case, use throwaway id
  102. eventId: '00000000000000000000000000000000',
  103. });
  104. trackAnalyticsInternal('user_feedback.dialog_opened');
  105. }}
  106. >
  107. {t('See an example')}
  108. </Button>
  109. </ButtonList>
  110. </OnboardingPanel>
  111. );
  112. }
  113. const ButtonList = styled(ButtonBar)`
  114. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  115. `;