setupDocsLoader.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import {Fragment, useCallback, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {motion} from 'framer-motion';
  4. import type {Location} from 'history';
  5. import beautify from 'js-beautify';
  6. import {Button} from 'sentry/components/button';
  7. import {CodeSnippet} from 'sentry/components/codeSnippet';
  8. import HookOrDefault from 'sentry/components/hookOrDefault';
  9. import ExternalLink from 'sentry/components/links/externalLink';
  10. import LoadingError from 'sentry/components/loadingError';
  11. import {DocumentationWrapper} from 'sentry/components/onboarding/documentationWrapper';
  12. import {
  13. ProductSelection,
  14. ProductSolution,
  15. } from 'sentry/components/onboarding/productSelection';
  16. import {IconChevron} from 'sentry/icons';
  17. import {t} from 'sentry/locale';
  18. import {space} from 'sentry/styles/space';
  19. import type {Organization, PlatformKey, Project, ProjectKey} from 'sentry/types';
  20. import {trackAnalytics} from 'sentry/utils/analytics';
  21. import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
  22. import {decodeList} from 'sentry/utils/queryString';
  23. import useApi from 'sentry/utils/useApi';
  24. const ProductSelectionAvailabilityHook = HookOrDefault({
  25. hookName: 'component:product-selection-availability',
  26. defaultComponent: ProductSelection,
  27. });
  28. export function SetupDocsLoader({
  29. organization,
  30. location,
  31. project,
  32. platform,
  33. close,
  34. }: {
  35. close: () => void;
  36. location: Location;
  37. organization: Organization;
  38. platform: PlatformKey | null;
  39. project: Project;
  40. }) {
  41. const api = useApi();
  42. const currentPlatform = platform ?? project?.platform ?? 'other';
  43. const [projectKey, setProjectKey] = useState<ProjectKey | null>(null);
  44. const [hasLoadingError, setHasLoadingError] = useState(false);
  45. const [projectKeyUpdateError, setProjectKeyUpdateError] = useState(false);
  46. const productsQuery =
  47. (location.query.product as ProductSolution | ProductSolution[] | undefined) ?? [];
  48. const products = decodeList(productsQuery) as ProductSolution[];
  49. const fetchData = useCallback(async () => {
  50. const keysApiUrl = `/projects/${organization.slug}/${project.slug}/keys/`;
  51. try {
  52. const loadedKeys = await api.requestPromise(keysApiUrl);
  53. if (loadedKeys.length === 0) {
  54. setHasLoadingError(true);
  55. return;
  56. }
  57. setProjectKey(loadedKeys[0]);
  58. setHasLoadingError(false);
  59. } catch (error) {
  60. setHasLoadingError(error);
  61. throw error;
  62. }
  63. }, [api, organization.slug, project.slug]);
  64. // Automatically update the products on the project key when the user changes the product selection
  65. // Note that on initial visit, this will also update the project key with the default products (=all products)
  66. // This DOES NOT take into account any initial products that may already be set on the project key - they will always be overwritten!
  67. const handleUpdateSelectedProducts = useCallback(async () => {
  68. const keyId = projectKey?.id;
  69. if (!keyId) {
  70. return;
  71. }
  72. const newDynamicSdkLoaderOptions: ProjectKey['dynamicSdkLoaderOptions'] = {
  73. hasPerformance: false,
  74. hasReplay: false,
  75. hasDebug: false,
  76. };
  77. products.forEach(product => {
  78. // eslint-disable-next-line default-case
  79. switch (product) {
  80. case ProductSolution.PERFORMANCE_MONITORING:
  81. newDynamicSdkLoaderOptions.hasPerformance = true;
  82. break;
  83. case ProductSolution.SESSION_REPLAY:
  84. newDynamicSdkLoaderOptions.hasReplay = true;
  85. break;
  86. }
  87. });
  88. try {
  89. await api.requestPromise(
  90. `/projects/${organization.slug}/${project.slug}/keys/${keyId}/`,
  91. {
  92. method: 'PUT',
  93. data: {
  94. dynamicSdkLoaderOptions: newDynamicSdkLoaderOptions,
  95. },
  96. }
  97. );
  98. setProjectKeyUpdateError(false);
  99. } catch (error) {
  100. const message = t('Unable to updated dynamic SDK loader configuration');
  101. handleXhrErrorResponse(message, error);
  102. setProjectKeyUpdateError(true);
  103. }
  104. }, [api, organization.slug, project.slug, projectKey?.id, products]);
  105. const track = useCallback(() => {
  106. if (!project?.id) {
  107. return;
  108. }
  109. trackAnalytics('onboarding.setup_loader_docs_rendered', {
  110. organization,
  111. platform: currentPlatform,
  112. project_id: project?.id,
  113. });
  114. }, [organization, currentPlatform, project?.id]);
  115. useEffect(() => {
  116. fetchData();
  117. }, [fetchData, organization.slug, project.slug]);
  118. useEffect(() => {
  119. handleUpdateSelectedProducts();
  120. }, [handleUpdateSelectedProducts, location.query.product]);
  121. useEffect(() => {
  122. track();
  123. }, [track]);
  124. return (
  125. <Fragment>
  126. <Header>
  127. <ProductSelectionAvailabilityHook
  128. organization={organization}
  129. lazyLoader
  130. skipLazyLoader={close}
  131. platform={currentPlatform}
  132. />
  133. </Header>
  134. <Divider />
  135. {projectKeyUpdateError && (
  136. <LoadingError
  137. message={t('Failed to update the project key with the selected products.')}
  138. onRetry={handleUpdateSelectedProducts}
  139. />
  140. )}
  141. {!hasLoadingError ? (
  142. projectKey !== null && (
  143. <ProjectKeyInfo
  144. projectKey={projectKey}
  145. platform={platform}
  146. organization={organization}
  147. project={project}
  148. products={products}
  149. />
  150. )
  151. ) : (
  152. <LoadingError
  153. message={t('Failed to load Client Keys for the project.')}
  154. onRetry={fetchData}
  155. />
  156. )}
  157. </Fragment>
  158. );
  159. }
  160. function ProjectKeyInfo({
  161. projectKey,
  162. platform,
  163. organization,
  164. project,
  165. products,
  166. }: {
  167. organization: Organization;
  168. platform: PlatformKey | null;
  169. products: ProductSolution[];
  170. project: Project;
  171. projectKey: ProjectKey;
  172. }) {
  173. const [showOptionalConfig, setShowOptionalConfig] = useState(false);
  174. const loaderLink = projectKey.dsn.cdn;
  175. const currentPlatform = platform ?? project?.platform ?? 'other';
  176. const hasPerformance = products.includes(ProductSolution.PERFORMANCE_MONITORING);
  177. const hasSessionReplay = products.includes(ProductSolution.SESSION_REPLAY);
  178. const configCodeSnippet = beautify.html(
  179. `<script>
  180. Sentry.onLoad(function() {
  181. Sentry.init({${
  182. !(hasPerformance || hasSessionReplay)
  183. ? `
  184. // You can add any additional configuration here`
  185. : ''
  186. }${
  187. hasPerformance
  188. ? `
  189. // Performance Monitoring
  190. tracesSampleRate: 1.0, // Capture 100% of the transactions`
  191. : ''
  192. }${
  193. hasSessionReplay
  194. ? `
  195. // Session Replay
  196. replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
  197. replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.`
  198. : ''
  199. }
  200. });
  201. });
  202. </script>`,
  203. {indent_size: 2}
  204. );
  205. const verifyCodeSnippet = beautify.html(
  206. `<script>
  207. myUndefinedFunction();
  208. </script>`,
  209. {indent_size: 2}
  210. );
  211. const toggleOptionalConfiguration = useCallback(() => {
  212. const show = !showOptionalConfig;
  213. setShowOptionalConfig(show);
  214. if (show) {
  215. trackAnalytics('onboarding.js_loader_optional_configuration_shown', {
  216. organization,
  217. platform: currentPlatform,
  218. project_id: project.id,
  219. });
  220. }
  221. }, [organization, project.id, currentPlatform, showOptionalConfig]);
  222. return (
  223. <DocsWrapper>
  224. <DocumentationWrapper>
  225. <h2>{t('Install')}</h2>
  226. <p>{t('Add this script tag to the top of the page:')}</p>
  227. <CodeSnippet dark language="html">
  228. {beautify.html(
  229. `<script src="${loaderLink}" crossorigin="anonymous"></script>`,
  230. {indent_size: 2, wrap_attributes: 'force-expand-multiline'}
  231. )}
  232. </CodeSnippet>
  233. <OptionalConfigWrapper>
  234. <ToggleButton
  235. priority="link"
  236. borderless
  237. size="zero"
  238. icon={<IconChevron direction={showOptionalConfig ? 'down' : 'right'} />}
  239. aria-label={t('Toggle optional configuration')}
  240. onClick={toggleOptionalConfiguration}
  241. />
  242. <h2 onClick={toggleOptionalConfiguration}>{t('Configuration (Optional)')}</h2>
  243. </OptionalConfigWrapper>
  244. {showOptionalConfig && (
  245. <div>
  246. <p>
  247. {t(
  248. "Initialize Sentry as early as possible in your application's lifecycle."
  249. )}
  250. </p>
  251. <CodeSnippet dark language="html">
  252. {configCodeSnippet}
  253. </CodeSnippet>
  254. </div>
  255. )}
  256. <h2>{t('Verify')}</h2>
  257. <p>
  258. {t(
  259. "This snippet contains an intentional error and can be used as a test to make sure that everything's working as expected."
  260. )}
  261. </p>
  262. <CodeSnippet dark language="html">
  263. {verifyCodeSnippet}
  264. </CodeSnippet>
  265. <hr />
  266. <h2>{t('Next Steps')}</h2>
  267. <ul>
  268. <li>
  269. <ExternalLink href="https://docs.sentry.io/platforms/javascript/sourcemaps/">
  270. {t('Source Maps')}
  271. </ExternalLink>
  272. {': '}
  273. {t('Learn how to enable readable stack traces in your Sentry errors.')}
  274. </li>
  275. <li>
  276. <ExternalLink href="https://docs.sentry.io/platforms/javascript/configuration/">
  277. {t('SDK Configuration')}
  278. </ExternalLink>
  279. {': '}
  280. {t('Learn how to configure your SDK using our Loader Script')}
  281. </li>
  282. {!products.includes(ProductSolution.PERFORMANCE_MONITORING) && (
  283. <li>
  284. <ExternalLink href="https://docs.sentry.io/platforms/javascript/performance/">
  285. {t('Performance Monitoring')}
  286. </ExternalLink>
  287. {': '}
  288. {t(
  289. 'Track down transactions to connect the dots between 10-second page loads and poor-performing API calls or slow database queries.'
  290. )}
  291. </li>
  292. )}
  293. {!products.includes(ProductSolution.SESSION_REPLAY) && (
  294. <li>
  295. <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/">
  296. {t('Session Replay')}
  297. </ExternalLink>
  298. {': '}
  299. {t(
  300. 'Get to the root cause of an error or latency issue faster by seeing all the technical details related to that issue in one visual replay on your web application.'
  301. )}
  302. </li>
  303. )}
  304. </ul>
  305. </DocumentationWrapper>
  306. </DocsWrapper>
  307. );
  308. }
  309. const DocsWrapper = styled(motion.div)``;
  310. const Header = styled('div')`
  311. display: flex;
  312. flex-direction: column;
  313. gap: ${space(2)};
  314. `;
  315. const OptionalConfigWrapper = styled('div')`
  316. display: flex;
  317. cursor: pointer;
  318. `;
  319. const ToggleButton = styled(Button)`
  320. &,
  321. :hover {
  322. color: ${p => p.theme.gray500};
  323. }
  324. `;
  325. const Divider = styled('hr')<{withBottomMargin?: boolean}>`
  326. height: 1px;
  327. width: 100%;
  328. background: ${p => p.theme.border};
  329. border: none;
  330. `;