replayOnboardingPanel.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import emptyStateImg from 'sentry-images/spot/replays-empty-state.svg';
  4. import Accordion from 'sentry/components/accordion/accordion';
  5. import Alert from 'sentry/components/alert';
  6. import {Button} from 'sentry/components/button';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import HookOrDefault from 'sentry/components/hookOrDefault';
  9. import ExternalLink from 'sentry/components/links/externalLink';
  10. import {useProjectCreationAccess} from 'sentry/components/projects/useProjectCreationAccess';
  11. import QuestionTooltip from 'sentry/components/questionTooltip';
  12. import {Tooltip} from 'sentry/components/tooltip';
  13. import {replayPlatforms} from 'sentry/data/platformCategories';
  14. import {IconInfo} from 'sentry/icons';
  15. import {t, tct} from 'sentry/locale';
  16. import PreferencesStore from 'sentry/stores/preferencesStore';
  17. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  18. import {space} from 'sentry/styles/space';
  19. import {useReplayOnboardingSidebarPanel} from 'sentry/utils/replays/hooks/useReplayOnboarding';
  20. import useOrganization from 'sentry/utils/useOrganization';
  21. import usePageFilters from 'sentry/utils/usePageFilters';
  22. import useProjects from 'sentry/utils/useProjects';
  23. import {HeaderContainer, WidgetContainer} from 'sentry/views/profiling/landing/styles';
  24. import ReplayPanel from 'sentry/views/replays/list/replayPanel';
  25. type Breakpoints = {
  26. large: string;
  27. medium: string;
  28. small: string;
  29. xlarge: string;
  30. };
  31. const OnboardingCTAHook = HookOrDefault({
  32. hookName: 'component:replay-onboarding-cta',
  33. defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
  34. });
  35. const OnboardingCTAButton = HookOrDefault({
  36. hookName: 'component:replay-onboarding-cta-button',
  37. defaultComponent: null,
  38. });
  39. const OnboardingAlertHook = HookOrDefault({
  40. hookName: 'component:replay-onboarding-alert',
  41. defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
  42. });
  43. export default function ReplayOnboardingPanel() {
  44. const preferences = useLegacyStore(PreferencesStore);
  45. const pageFilters = usePageFilters();
  46. const projects = useProjects();
  47. const organization = useOrganization();
  48. const {canCreateProject} = useProjectCreationAccess({organization});
  49. const selectedProjects = projects.projects.filter(p =>
  50. pageFilters.selection.projects.includes(Number(p.id))
  51. );
  52. const hasSelectedProjects = selectedProjects.length > 0;
  53. const allProjectsUnsupported = projects.projects.every(
  54. p => !replayPlatforms.includes(p.platform!)
  55. );
  56. const allSelectedProjectsUnsupported = selectedProjects.every(
  57. p => !replayPlatforms.includes(p.platform!)
  58. );
  59. // if all projects are unsupported we should prompt the user to create a project
  60. // else we prompt to setup
  61. const primaryAction = allProjectsUnsupported ? 'create' : 'setup';
  62. // disable "create" if the user has insufficient permissions
  63. // disable "setup" if the current selected pageFilters are not supported
  64. const primaryActionDisabled =
  65. primaryAction === 'create'
  66. ? !canCreateProject
  67. : allSelectedProjectsUnsupported && hasSelectedProjects;
  68. const breakpoints = preferences.collapsed
  69. ? {
  70. small: '800px',
  71. medium: '992px',
  72. large: '1210px',
  73. xlarge: '1450px',
  74. }
  75. : {
  76. small: '800px',
  77. medium: '1175px',
  78. large: '1375px',
  79. xlarge: '1450px',
  80. };
  81. return (
  82. <Fragment>
  83. <OnboardingAlertHook>
  84. {hasSelectedProjects && allSelectedProjectsUnsupported && (
  85. <Alert icon={<IconInfo />}>
  86. {tct(
  87. `[projectMsg] [action] a project using our [link], or equivalent framework SDK.`,
  88. {
  89. action: primaryAction === 'create' ? t('Create') : t('Select'),
  90. projectMsg: (
  91. <strong>
  92. {t(
  93. `Session Replay isn't available for project %s.`,
  94. selectedProjects[0].slug
  95. )}
  96. </strong>
  97. ),
  98. link: (
  99. <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/">
  100. {t('Sentry browser SDK package')}
  101. </ExternalLink>
  102. ),
  103. }
  104. )}
  105. </Alert>
  106. )}
  107. </OnboardingAlertHook>
  108. <ReplayPanel image={<HeroImage src={emptyStateImg} breakpoints={breakpoints} />}>
  109. <OnboardingCTAHook organization={organization}>
  110. <SetupReplaysCTA
  111. orgSlug={organization.slug}
  112. primaryAction={primaryAction}
  113. disabled={primaryActionDisabled}
  114. />
  115. </OnboardingCTAHook>
  116. </ReplayPanel>
  117. </Fragment>
  118. );
  119. }
  120. interface SetupReplaysCTAProps {
  121. orgSlug: string;
  122. primaryAction: 'setup' | 'create';
  123. disabled?: boolean;
  124. }
  125. export function SetupReplaysCTA({
  126. disabled,
  127. primaryAction = 'setup',
  128. orgSlug,
  129. }: SetupReplaysCTAProps) {
  130. const {activateSidebar} = useReplayOnboardingSidebarPanel();
  131. const [expanded, setExpanded] = useState(-1);
  132. const FAQ = [
  133. {
  134. header: () => (
  135. <QuestionContent>{t('Can I use Session Replay with my app?')}</QuestionContent>
  136. ),
  137. content: () => (
  138. <AnswerContent>
  139. <div>
  140. {t(
  141. 'Session Replay supports all browser-based applications. This includes static websites, single-page aplications, and also server-side rendered applications. The only prerequisite is that your application uses Sentry JavaScript SDK (version 7.2.0 or greater) either with NPM/Yarn or with our JS Loader script.'
  142. )}
  143. </div>
  144. <div>
  145. {t(
  146. "Replays are integrated with Sentry's tracing data model, enabling you to see replays associated with backend errors as well. You need to have Sentry set up for both your frontend and backend, along with distributed tracing."
  147. )}
  148. </div>
  149. <div>
  150. {tct(
  151. 'To learn more about which SDKs we support, please visit [link:our docs].',
  152. {
  153. link: (
  154. <ExternalLink href="https://docs.sentry.io/product/session-replay/getting-started/" />
  155. ),
  156. }
  157. )}
  158. </div>
  159. </AnswerContent>
  160. ),
  161. },
  162. {
  163. header: () => (
  164. <QuestionContent>{t('What’s the performance overhead?')}</QuestionContent>
  165. ),
  166. content: () => (
  167. <AnswerContent>
  168. <div>
  169. {t(
  170. 'Session Replay adds a small amount of performance overhead to your web application. For most web apps, the performance overhead of our client SDK will be imperceptible to end-users. For example, the Sentry site has Replay enabled and we have not seen any significant slowdowns.'
  171. )}
  172. </div>
  173. <div>
  174. {t(
  175. 'The performance overhead generally scales linearly with the DOM complexity of your application. The more DOM state changes that occur in the application lifecycle, the more events that are captured, transmitted, etc.'
  176. )}
  177. </div>
  178. <div>
  179. {tct(
  180. 'To learn more about how we’ve optimized our SDK, please visit [link:our docs].',
  181. {
  182. link: (
  183. <ExternalLink href="https://docs.sentry.io/product/session-replay/performance-overhead/" />
  184. ),
  185. }
  186. )}
  187. </div>
  188. </AnswerContent>
  189. ),
  190. },
  191. {
  192. header: () => (
  193. <QuestionContent>{t('How do you protect user data?')}</QuestionContent>
  194. ),
  195. content: () => (
  196. <AnswerContent>
  197. <div>
  198. {t(
  199. 'We offer a range of privacy controls to let developers ensure that no sensitive user information leaves the browser. By default, our privacy configuration is very aggressive and masks all text and images, but you can choose to just mask user input text, for example.'
  200. )}
  201. </div>
  202. <div>
  203. {t(
  204. 'Customers can also use server-side scrubbing capabilities to further filter and remove sensitive user data, or our deletion capabilities to delete individual recordings after ingestion.'
  205. )}
  206. </div>
  207. <div>
  208. {tct(
  209. 'To learn more about how we protect user privacy, please visit [link:our docs].',
  210. {
  211. link: (
  212. <ExternalLink href="https://docs.sentry.io/product/session-replay/protecting-user-privacy/" />
  213. ),
  214. }
  215. )}
  216. </div>
  217. </AnswerContent>
  218. ),
  219. },
  220. ];
  221. function renderCTA() {
  222. if (primaryAction === 'setup') {
  223. return (
  224. <Tooltip
  225. title={
  226. <span data-test-id="setup-replays-tooltip">
  227. {t('Select a supported project from the projects dropdown.')}
  228. </span>
  229. }
  230. disabled={!disabled} // we only want to show the tooltip when the button is disabled
  231. >
  232. <Button
  233. data-test-id="setup-replays-btn"
  234. onClick={activateSidebar}
  235. priority="primary"
  236. disabled={disabled}
  237. >
  238. {t('Set Up Replays')}
  239. </Button>
  240. </Tooltip>
  241. );
  242. }
  243. return (
  244. <Tooltip
  245. title={
  246. <span data-test-id="create-project-tooltip">
  247. {t('You do not have permission to create a project.')}
  248. </span>
  249. }
  250. disabled={!disabled}
  251. >
  252. <Button
  253. data-test-id="create-project-btn"
  254. to={`/organizations/${orgSlug}/projects/new/`}
  255. priority="primary"
  256. disabled={disabled}
  257. >
  258. {t('Create Project')}
  259. </Button>
  260. </Tooltip>
  261. );
  262. }
  263. return (
  264. <CenteredContent>
  265. <h3>{t('Get to the root cause faster')}</h3>
  266. <p>
  267. {t(
  268. 'See a video-like reproduction of your user sessions so you can see what happened before, during, and after an error or latency issue occurred.'
  269. )}
  270. </p>
  271. <ButtonList gap={1}>
  272. {renderCTA()}
  273. <OnboardingCTAButton />
  274. <Button
  275. href="https://docs.sentry.io/platforms/javascript/session-replay/"
  276. external
  277. >
  278. {t('Read Docs')}
  279. </Button>
  280. </ButtonList>
  281. <StyledWidgetContainer>
  282. <StyledHeaderContainer>
  283. {t('FAQ')}
  284. <QuestionTooltip
  285. size="xs"
  286. isHoverable
  287. title={tct('See a [link:full list of FAQs].', {
  288. link: (
  289. <ExternalLink href="https://help.sentry.io/product-features/other/what-is-session-replay/" />
  290. ),
  291. })}
  292. />
  293. </StyledHeaderContainer>
  294. <Accordion
  295. items={FAQ}
  296. expandedIndex={expanded}
  297. setExpandedIndex={setExpanded}
  298. buttonOnLeft
  299. />
  300. </StyledWidgetContainer>
  301. </CenteredContent>
  302. );
  303. }
  304. const HeroImage = styled('img')<{breakpoints: Breakpoints}>`
  305. @media (min-width: ${p => p.breakpoints.small}) {
  306. user-select: none;
  307. position: absolute;
  308. top: 0;
  309. bottom: 0;
  310. width: 220px;
  311. margin-top: auto;
  312. margin-bottom: auto;
  313. transform: translateX(-50%);
  314. left: 50%;
  315. }
  316. @media (min-width: ${p => p.breakpoints.medium}) {
  317. transform: translateX(-55%);
  318. width: 300px;
  319. min-width: 300px;
  320. }
  321. @media (min-width: ${p => p.breakpoints.large}) {
  322. transform: translateX(-60%);
  323. width: 380px;
  324. min-width: 380px;
  325. }
  326. @media (min-width: ${p => p.breakpoints.xlarge}) {
  327. transform: translateX(-65%);
  328. width: 420px;
  329. min-width: 420px;
  330. }
  331. `;
  332. const ButtonList = styled(ButtonBar)`
  333. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  334. `;
  335. const StyledWidgetContainer = styled(WidgetContainer)`
  336. margin: ${space(4)} 0 ${space(1)} 0;
  337. `;
  338. const CenteredContent = styled('div')`
  339. padding: ${space(3)};
  340. `;
  341. const AnswerContent = styled('div')`
  342. display: grid;
  343. gap: ${space(2)};
  344. padding: ${space(2)};
  345. `;
  346. const QuestionContent = styled('div')`
  347. font-weight: bold;
  348. cursor: pointer;
  349. `;
  350. const StyledHeaderContainer = styled(HeaderContainer)`
  351. font-weight: bold;
  352. font-size: ${p => p.theme.fontSizeLarge};
  353. color: ${p => p.theme.gray300};
  354. display: flex;
  355. gap: ${space(0.5)};
  356. align-items: center;
  357. `;