setupDocsLoader.tsx 10 KB

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