vitalsAlertCTA.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import styled from '@emotion/styled';
  2. import {promptsUpdate} from 'sentry/actionCreators/prompts';
  3. import {
  4. NotificationBar,
  5. StyledNotificationBarIconInfo,
  6. } from 'sentry/components/alerts/notificationBar';
  7. import Button from 'sentry/components/button';
  8. import ButtonBar from 'sentry/components/buttonBar';
  9. import ExternalLink from 'sentry/components/links/externalLink';
  10. import {IconClose} from 'sentry/icons';
  11. import {t, tct} from 'sentry/locale';
  12. import {Organization} from 'sentry/types';
  13. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  14. import useApi from 'sentry/utils/useApi';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. import {INDUSTRY_STANDARDS, SENTRY_CUSTOMERS} from './constants';
  17. import {VitalsKey, VitalsResult} from './types';
  18. import {getRelativeDiff, getWorstVital} from './utils';
  19. interface Props {
  20. data: VitalsResult;
  21. dismissAlert: () => void;
  22. }
  23. function getPercentage(diff: number) {
  24. return <strong>{Math.abs(Math.round(diff * 100))}%</strong>;
  25. }
  26. function getDocsLink(vital: VitalsKey) {
  27. switch (vital) {
  28. case 'FCP':
  29. return 'https://docs.sentry.io/product/performance/web-vitals/#first-contentful-paint-fcp';
  30. case 'LCP':
  31. return 'https://docs.sentry.io/product/performance/web-vitals/#largest-contentful-paint-lcp';
  32. default:
  33. // just one link for mobile vitals
  34. return 'https://docs.sentry.io/product/performance/mobile-vitals/#app-start';
  35. }
  36. }
  37. function getVitalsType(vital: VitalsKey) {
  38. return ['FCP', 'LCP'].includes(vital) ? 'web' : 'mobile';
  39. }
  40. function getVitalWithLink({
  41. vital,
  42. organization,
  43. }: {
  44. organization: Organization;
  45. vital: VitalsKey;
  46. }) {
  47. const url = new URL(getDocsLink(vital));
  48. url.searchParams.append('referrer', 'vitals-alert');
  49. return (
  50. <ExternalLink
  51. onClick={() => {
  52. trackAdvancedAnalyticsEvent('vitals_alert.clicked_docs', {vital, organization});
  53. }}
  54. href={url.toString()}
  55. >
  56. {vital}
  57. </ExternalLink>
  58. );
  59. }
  60. export default function VitalsAlertCTA({data, dismissAlert}: Props) {
  61. const organization = useOrganization();
  62. // persist to dismiss alert
  63. const api = useApi({persistInFlight: true});
  64. const vital = getWorstVital(data);
  65. if (!vital) {
  66. return null;
  67. }
  68. const ourValue = data[vital];
  69. if (!ourValue) {
  70. return null;
  71. }
  72. const sentryDiff = getRelativeDiff(ourValue, SENTRY_CUSTOMERS[vital]);
  73. const industryDiff = getRelativeDiff(ourValue, INDUSTRY_STANDARDS[vital]);
  74. // if worst vital is better than Sentry users, we shouldn't show this alert
  75. if (sentryDiff < 0) {
  76. return null;
  77. }
  78. const industryDiffPercentage = getPercentage(industryDiff);
  79. const sentryDiffPercentage = getPercentage(sentryDiff);
  80. const vitalsType = getVitalsType(vital);
  81. const getText = () => {
  82. const args = {
  83. vital: getVitalWithLink({vital, organization}),
  84. industryDiffPercentage,
  85. sentryDiffPercentage,
  86. };
  87. // different language if we are better than the industry average
  88. if (industryDiff < 0) {
  89. return tct(
  90. "Your organization's [vital] is [industryDiffPercentage] lower than the industry standard, but [sentryDiffPercentage] higher than typical Sentry users.",
  91. args
  92. );
  93. }
  94. return tct(
  95. "Your organization's [vital] is [industryDiffPercentage] higher than the industry standard and [sentryDiffPercentage] higher than typical Sentry users.",
  96. args
  97. );
  98. };
  99. const getVitalsURL = () => {
  100. // TODO: add logic for project selection
  101. const performanceRoot = `/organizations/${organization.slug}/performance`;
  102. const baseParams = {
  103. statsPeriod: '7d',
  104. referrer: `vitals-alert-${vital.toLowerCase()}`,
  105. };
  106. // we can land on a specific web vital
  107. if (vitalsType === 'web') {
  108. const searchParams = new URLSearchParams({
  109. ...baseParams,
  110. vitalName: `measurements.${vital.toLowerCase()}`,
  111. });
  112. return `${performanceRoot}/vitaldetail/?${searchParams}`;
  113. }
  114. // otherwise it's just the mobile vital screen
  115. const searchParams = new URLSearchParams({
  116. ...baseParams,
  117. landingDisplay: 'mobile',
  118. });
  119. return `${performanceRoot}/?${searchParams}`;
  120. };
  121. const buttonText = vitalsType === 'web' ? t('See Web Vitals') : t('See Mobile Vitals');
  122. const dismissAndPromptUpdate = () => {
  123. promptsUpdate(api, {
  124. organizationId: organization?.id,
  125. feature: 'vitals_alert',
  126. status: 'dismissed',
  127. });
  128. dismissAlert();
  129. };
  130. return (
  131. <NotificationBar>
  132. <StyledNotificationBarIconInfo />
  133. {getText()}
  134. <NotificationBarButtons gap={1}>
  135. <Button
  136. to={getVitalsURL()}
  137. size="xs"
  138. onClick={() => {
  139. dismissAndPromptUpdate();
  140. trackAdvancedAnalyticsEvent('vitals_alert.clicked_see_vitals', {
  141. vital,
  142. organization,
  143. });
  144. }}
  145. >
  146. {buttonText}
  147. </Button>
  148. <Button
  149. icon={<IconClose />}
  150. onClick={() => {
  151. dismissAndPromptUpdate();
  152. trackAdvancedAnalyticsEvent('vitals_alert.dismissed', {
  153. vital,
  154. organization,
  155. });
  156. }}
  157. size="xs"
  158. priority="link"
  159. title={t('Dismiss')}
  160. aria-label={t('Dismiss')}
  161. />
  162. </NotificationBarButtons>
  163. </NotificationBar>
  164. );
  165. }
  166. const NotificationBarButtons = styled(ButtonBar)`
  167. margin-left: auto;
  168. white-space: nowrap;
  169. `;