loaderSettings.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import {Fragment, useCallback, useState} from 'react';
  2. import {
  3. addErrorMessage,
  4. addLoadingMessage,
  5. addSuccessMessage,
  6. } from 'sentry/actionCreators/indicator';
  7. import Access from 'sentry/components/acl/access';
  8. import FieldGroup from 'sentry/components/forms/fieldGroup';
  9. import BooleanField from 'sentry/components/forms/fields/booleanField';
  10. import SelectField from 'sentry/components/forms/fields/selectField';
  11. import ExternalLink from 'sentry/components/links/externalLink';
  12. import TextCopyInput from 'sentry/components/textCopyInput';
  13. import {t, tct} from 'sentry/locale';
  14. import type {Project, ProjectKey} from 'sentry/types/project';
  15. import getDynamicText from 'sentry/utils/getDynamicText';
  16. import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
  17. import useApi from 'sentry/utils/useApi';
  18. type Props = {
  19. data: ProjectKey;
  20. keyId: string;
  21. orgSlug: string;
  22. project: Project;
  23. updateData: (data: ProjectKey) => void;
  24. };
  25. export function LoaderSettings({keyId, orgSlug, project, data, updateData}: Props) {
  26. const api = useApi();
  27. const [requestPending, setRequestPending] = useState(false);
  28. const [optimisticState, setOptimisticState] = useState({
  29. browserSdkVersion: data.browserSdkVersion,
  30. hasDebug: data.dynamicSdkLoaderOptions.hasDebug,
  31. hasPerformance: data.dynamicSdkLoaderOptions.hasPerformance,
  32. hasReplay: data.dynamicSdkLoaderOptions.hasReplay,
  33. });
  34. const values = requestPending
  35. ? optimisticState
  36. : {
  37. browserSdkVersion:
  38. // "latest" was an option that we don't let users select anymore. It will be phased out when version v8 of
  39. // the SDK is released, meaning we want to map the backend's response to v7 when it responds with "latest".
  40. // "7.x" was the "latest" version when "latest" was phased out.
  41. data.browserSdkVersion === 'latest' ? '7.x' : data.browserSdkVersion,
  42. hasDebug: data.dynamicSdkLoaderOptions.hasDebug,
  43. hasPerformance: data.dynamicSdkLoaderOptions.hasPerformance,
  44. hasReplay: data.dynamicSdkLoaderOptions.hasReplay,
  45. };
  46. const sdkVersionChoices = data.browserSdk
  47. ? // "latest" was an option that we do not want to allow users to select anymore. It was phased out with v7, before v8 was released.
  48. data.browserSdk.choices.filter(([value]) => value !== 'latest')
  49. : [];
  50. const apiEndpoint = `/projects/${orgSlug}/${project.slug}/keys/${keyId}/`;
  51. const loaderLink = getDynamicText({
  52. value: data.dsn.cdn,
  53. fixed: '__JS_SDK_LOADER_URL__',
  54. });
  55. const updateLoaderOption = useCallback(
  56. async (changes: {
  57. browserSdkVersion?: string;
  58. hasDebug?: boolean;
  59. hasPerformance?: boolean;
  60. hasReplay?: boolean;
  61. }) => {
  62. setRequestPending(true);
  63. setOptimisticState({
  64. browserSdkVersion: data.browserSdkVersion,
  65. hasDebug: data.dynamicSdkLoaderOptions.hasDebug,
  66. hasPerformance: data.dynamicSdkLoaderOptions.hasPerformance,
  67. hasReplay: data.dynamicSdkLoaderOptions.hasReplay,
  68. ...changes,
  69. });
  70. addLoadingMessage();
  71. const browserSdkVersion = changes.browserSdkVersion ?? data.browserSdkVersion;
  72. let payload: any;
  73. if (sdkVersionSupportsPerformanceAndReplay(browserSdkVersion)) {
  74. payload = {
  75. browserSdkVersion,
  76. dynamicSdkLoaderOptions: {
  77. hasDebug: changes.hasDebug ?? data.dynamicSdkLoaderOptions.hasDebug,
  78. hasPerformance:
  79. changes.hasPerformance ?? data.dynamicSdkLoaderOptions.hasPerformance,
  80. hasReplay: changes.hasReplay ?? data.dynamicSdkLoaderOptions.hasReplay,
  81. },
  82. };
  83. } else {
  84. payload = {
  85. browserSdkVersion,
  86. dynamicSdkLoaderOptions: {
  87. hasDebug: changes.hasDebug ?? data.dynamicSdkLoaderOptions.hasDebug,
  88. hasPerformance: false,
  89. hasReplay: false,
  90. },
  91. };
  92. }
  93. try {
  94. const response = await api.requestPromise(apiEndpoint, {
  95. method: 'PUT',
  96. data: payload,
  97. });
  98. updateData(response);
  99. addSuccessMessage(t('Successfully updated dynamic SDK loader configuration'));
  100. } catch (error) {
  101. const message = t('Unable to updated dynamic SDK loader configuration');
  102. handleXhrErrorResponse(message, error);
  103. addErrorMessage(message);
  104. } finally {
  105. setRequestPending(false);
  106. }
  107. },
  108. [
  109. api,
  110. apiEndpoint,
  111. data.browserSdkVersion,
  112. data.dynamicSdkLoaderOptions.hasDebug,
  113. data.dynamicSdkLoaderOptions.hasPerformance,
  114. data.dynamicSdkLoaderOptions.hasReplay,
  115. setRequestPending,
  116. updateData,
  117. ]
  118. );
  119. return (
  120. <Access access={['project:write']} project={project}>
  121. {({hasAccess}) => (
  122. <Fragment>
  123. <FieldGroup
  124. help={tct(
  125. 'Copy this script into your website to setup your JavaScript SDK without any additional configuration. [link]',
  126. {
  127. link: (
  128. <ExternalLink href="https://docs.sentry.io/platforms/javascript/install/lazy-load-sentry/">
  129. {t(' What does the script provide?')}
  130. </ExternalLink>
  131. ),
  132. }
  133. )}
  134. inline={false}
  135. flexibleControlStateSize
  136. >
  137. <TextCopyInput aria-label={t('Loader Script')}>
  138. {`<script src="${loaderLink}" crossorigin="anonymous"></script>`}
  139. </TextCopyInput>
  140. </FieldGroup>
  141. <SelectField
  142. name={`${keyId}-browserSdkVersion`}
  143. label={t('SDK Version')}
  144. options={sdkVersionChoices.map(([value, label]) => ({
  145. value,
  146. label,
  147. }))}
  148. value={values.browserSdkVersion}
  149. onChange={value => {
  150. updateLoaderOption({browserSdkVersion: value});
  151. }}
  152. disabledReason={
  153. sdkVersionChoices.length === 1
  154. ? t(
  155. 'At the moment, only the shown SDK version is available. New versions of the SDK will appear here as soon as they are released, and you will be able to upgrade by selecting them.'
  156. )
  157. : undefined
  158. }
  159. placeholder="7.x"
  160. allowClear={false}
  161. disabled={!hasAccess || requestPending || sdkVersionChoices.length === 1}
  162. />
  163. <BooleanField
  164. label={t('Enable Performance Monitoring')}
  165. name={`${keyId}-has-performance`}
  166. value={
  167. sdkVersionSupportsPerformanceAndReplay(data.browserSdkVersion)
  168. ? values.hasPerformance
  169. : false
  170. }
  171. onChange={value => {
  172. updateLoaderOption({hasPerformance: value});
  173. }}
  174. disabled={
  175. !hasAccess ||
  176. requestPending ||
  177. !sdkVersionSupportsPerformanceAndReplay(data.browserSdkVersion)
  178. }
  179. help={
  180. !sdkVersionSupportsPerformanceAndReplay(data.browserSdkVersion)
  181. ? t('Only available in SDK version 7.x and above')
  182. : data.dynamicSdkLoaderOptions.hasPerformance
  183. ? tct(
  184. 'The default config is [codeTracesSampleRate:tracesSampleRate: 1.0] and distributed tracing to same-origin requests. [configDocs:Read the docs] to learn how to configure this.',
  185. {
  186. codeTracesSampleRate: <code />,
  187. configDocs: (
  188. <ExternalLink href="https://docs.sentry.io/platforms/javascript/install/loader/#custom-configuration" />
  189. ),
  190. }
  191. )
  192. : undefined
  193. }
  194. disabledReason={
  195. !hasAccess
  196. ? t('You do not have permission to edit this setting')
  197. : undefined
  198. }
  199. />
  200. <BooleanField
  201. label={t('Enable Session Replay')}
  202. name={`${keyId}-has-replay`}
  203. value={
  204. sdkVersionSupportsPerformanceAndReplay(data.browserSdkVersion)
  205. ? values.hasReplay
  206. : false
  207. }
  208. onChange={value => {
  209. updateLoaderOption({hasReplay: value});
  210. }}
  211. disabled={
  212. !hasAccess ||
  213. requestPending ||
  214. !sdkVersionSupportsPerformanceAndReplay(data.browserSdkVersion)
  215. }
  216. help={
  217. !sdkVersionSupportsPerformanceAndReplay(data.browserSdkVersion)
  218. ? t('Only available in SDK version 7.x and above')
  219. : data.dynamicSdkLoaderOptions.hasReplay
  220. ? tct(
  221. `[es5Warning]The default config is [codeReplay:replaysSessionSampleRate: 0.1] and [codeError:replaysOnErrorSampleRate: 1]. [configDocs:Read the docs] to learn how to configure this.`,
  222. {
  223. es5Warning:
  224. // latest is deprecated but resolves to v7
  225. data.browserSdkVersion === '7.x' ||
  226. data.browserSdkVersion === 'latest'
  227. ? t(
  228. 'When using Replay, the loader will load the ES6 bundle instead of the ES5 bundle.'
  229. ) + ' '
  230. : '',
  231. codeReplay: <code />,
  232. codeError: <code />,
  233. configDocs: (
  234. <ExternalLink href="https://docs.sentry.io/platforms/javascript/install/loader/#custom-configuration" />
  235. ),
  236. }
  237. )
  238. : undefined
  239. }
  240. disabledReason={
  241. !hasAccess
  242. ? t('You do not have permission to edit this setting')
  243. : undefined
  244. }
  245. />
  246. <BooleanField
  247. label={t('Enable Debug Bundles & Logging')}
  248. name={`${keyId}-has-logging`}
  249. value={values.hasDebug}
  250. onChange={value => {
  251. updateLoaderOption({hasDebug: value});
  252. }}
  253. disabled={!hasAccess || requestPending}
  254. disabledReason={
  255. !hasAccess
  256. ? t('You do not have permission to edit this setting')
  257. : undefined
  258. }
  259. />
  260. </Fragment>
  261. )}
  262. </Access>
  263. );
  264. }
  265. function sdkVersionSupportsPerformanceAndReplay(sdkVersion: string): boolean {
  266. return sdkVersion === 'latest' || sdkVersion === '7.x' || sdkVersion === '8.x';
  267. }