onboarding.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import styled from '@emotion/styled';
  2. import Alert from 'sentry/components/alert';
  3. import {Button} from 'sentry/components/button';
  4. import {CodeSnippet} from 'sentry/components/codeSnippet';
  5. import ExternalLink from 'sentry/components/links/externalLink';
  6. import TextCopyInput from 'sentry/components/textCopyInput';
  7. import {IconClose, IconInfo} from 'sentry/icons';
  8. import {t, tct} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import type {SpanFrame} from 'sentry/utils/replays/types';
  11. import useDismissAlert from 'sentry/utils/useDismissAlert';
  12. import useOrganization from 'sentry/utils/useOrganization';
  13. import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate';
  14. import {Output} from 'sentry/views/replays/detail/network/details/getOutputType';
  15. import type {TabKey} from 'sentry/views/replays/detail/network/details/tabs';
  16. export const useDismissReqRespBodiesAlert = () => {
  17. const organization = useOrganization();
  18. return useDismissAlert({
  19. key: `${organization.id}:replay-network-bodies-alert-dismissed`,
  20. });
  21. };
  22. export function ReqRespBodiesAlert({
  23. isNetworkDetailsSetup,
  24. }: {
  25. isNetworkDetailsSetup: boolean;
  26. }) {
  27. const {dismiss, isDismissed} = useDismissReqRespBodiesAlert();
  28. if (isDismissed) {
  29. return null;
  30. }
  31. const message = isNetworkDetailsSetup
  32. ? tct(
  33. 'Click on a [fetch] or [xhr] request to see request and response bodies. [link].',
  34. {
  35. fetch: <code>fetch</code>,
  36. xhr: <code>xhr</code>,
  37. link: (
  38. <ExternalLink
  39. href="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details"
  40. onClick={dismiss}
  41. >
  42. {t('Learn More')}
  43. </ExternalLink>
  44. ),
  45. }
  46. )
  47. : tct('Start collecting the body of requests and responses. [link].', {
  48. link: (
  49. <ExternalLink
  50. href="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details"
  51. onClick={dismiss}
  52. >
  53. {t('Learn More')}
  54. </ExternalLink>
  55. ),
  56. });
  57. return (
  58. <StyledAlert
  59. icon={<IconInfo />}
  60. opaque={false}
  61. showIcon
  62. type="info"
  63. trailingItems={
  64. <StyledButton priority="link" size="sm" onClick={dismiss}>
  65. <IconClose color="gray500" size="sm" />
  66. </StyledButton>
  67. }
  68. >
  69. {message}
  70. </StyledAlert>
  71. );
  72. }
  73. const StyledAlert = styled(Alert)`
  74. margin-bottom: ${space(1)};
  75. `;
  76. const StyledButton = styled(Button)`
  77. color: inherit;
  78. `;
  79. export function UnsupportedOp({type}: {type: 'headers' | 'bodies'}) {
  80. const title =
  81. type === 'bodies'
  82. ? t('Capture Request and Response Bodies')
  83. : t('Capture Request and Response Headers');
  84. return (
  85. <StyledInstructions data-test-id="network-op-unsupported">
  86. <h1>{title}</h1>
  87. <p>
  88. {tct(
  89. `This feature is only compatible with [fetch] and [xhr] request types. [link].`,
  90. {
  91. fetch: <code>fetch</code>,
  92. xhr: <code>xhr</code>,
  93. link: (
  94. <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details">
  95. {t('Learn more')}
  96. </ExternalLink>
  97. ),
  98. }
  99. )}
  100. </p>
  101. </StyledInstructions>
  102. );
  103. }
  104. export function Setup({
  105. item,
  106. projectId,
  107. showSnippet,
  108. visibleTab,
  109. }: {
  110. item: SpanFrame;
  111. projectId: string;
  112. showSnippet: Output;
  113. visibleTab: TabKey;
  114. }) {
  115. const organization = useOrganization();
  116. const {isFetching, needsUpdate} = useProjectSdkNeedsUpdate({
  117. // Only show update instructions if not >= 7.50.0, but our instructions
  118. // will show a different min version as there are known bugs in 7.50 ->
  119. // 7.53
  120. minVersion: '7.50.0',
  121. organization,
  122. projectId: [projectId],
  123. });
  124. const sdkNeedsUpdate = !isFetching && Boolean(needsUpdate);
  125. const url = item.description || 'http://example.com';
  126. return (
  127. <SetupInstructions
  128. minVersion="7.53.1"
  129. sdkNeedsUpdate={sdkNeedsUpdate}
  130. showSnippet={showSnippet}
  131. url={url}
  132. visibleTab={visibleTab}
  133. />
  134. );
  135. }
  136. function SetupInstructions({
  137. minVersion,
  138. sdkNeedsUpdate,
  139. showSnippet,
  140. url,
  141. visibleTab,
  142. }: {
  143. minVersion: string;
  144. sdkNeedsUpdate: boolean;
  145. showSnippet: Output;
  146. url: string;
  147. visibleTab: TabKey;
  148. }) {
  149. if (showSnippet === Output.DATA && visibleTab === 'details') {
  150. return (
  151. <NoMarginAlert type="muted" system data-test-id="network-setup-steps">
  152. {tct(
  153. 'You can capture additional headers by adding them to the [requestConfig] and [responseConfig] lists in your SDK config.',
  154. {
  155. requestConfig: <code>networkRequestHeaders</code>,
  156. responseConfig: <code>networkResponseHeaders</code>,
  157. }
  158. )}
  159. </NoMarginAlert>
  160. );
  161. }
  162. function trimUrl(oldUrl: string): string {
  163. const end = oldUrl.indexOf('?') > 0 ? oldUrl.indexOf('?') : oldUrl.length;
  164. return oldUrl.substring(0, end);
  165. }
  166. const urlSnippet = `
  167. networkDetailAllowUrls: ['${trimUrl(url)}'],`;
  168. const headersSnippet = `
  169. networkRequestHeaders: ['X-Custom-Header'],
  170. networkResponseHeaders: ['X-Custom-Header'],`;
  171. const includeHeadersSnippet =
  172. showSnippet === Output.SETUP ||
  173. ([Output.URL_SKIPPED, Output.DATA].includes(showSnippet) && visibleTab === 'details');
  174. const code = `Sentry.init({
  175. integrations: [
  176. new Replay({${urlSnippet + (includeHeadersSnippet ? headersSnippet : '')}
  177. }),
  178. ],
  179. })`;
  180. const title =
  181. showSnippet === Output.SETUP
  182. ? t('Capture Request and Response Headers and Bodies')
  183. : visibleTab === 'details'
  184. ? t('Capture Request and Response Headers')
  185. : t('Capture Request and Response Bodies');
  186. return (
  187. <StyledInstructions data-test-id="network-setup-steps">
  188. <h1>{title}</h1>
  189. <p>
  190. {tct(
  191. `To protect user privacy, Session Replay defaults to not capturing the request or response headers. However, we provide the option to do so, if it’s critical to your debugging process. [link].`,
  192. {
  193. link: (
  194. <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details">
  195. {t('Learn More')}
  196. </ExternalLink>
  197. ),
  198. }
  199. )}
  200. </p>
  201. <NetworkUrlWrapper>
  202. {showSnippet === Output.URL_SKIPPED &&
  203. url !== '[Filtered]' &&
  204. tct(
  205. 'Add the following to your [field] list to start capturing data: [alert] ',
  206. {
  207. field: <code>networkDetailAllowUrls</code>,
  208. alert: <StyledTextCopyInput>{trimUrl(url)}</StyledTextCopyInput>,
  209. }
  210. )}
  211. </NetworkUrlWrapper>
  212. {showSnippet === Output.BODY_SKIPPED && (
  213. <Alert type="warning">
  214. {tct('Enable [field] to capture both Request and Response bodies.', {
  215. field: <code>networkCaptureBodies: true</code>,
  216. })}
  217. </Alert>
  218. )}
  219. <h1>{t('Prerequisites')}</h1>
  220. <ol>
  221. {sdkNeedsUpdate ? (
  222. <li>
  223. {tct('Update your SDK version to >= [minVersion]', {
  224. minVersion,
  225. })}
  226. </li>
  227. ) : null}
  228. <li>{t('Edit the Replay integration configuration to allow this URL.')}</li>
  229. <li>{t('That’s it!')}</li>
  230. </ol>
  231. {url !== '[Filtered]' && (
  232. <CodeSnippet filename="JavaScript" language="javascript">
  233. {code}
  234. </CodeSnippet>
  235. )}
  236. </StyledInstructions>
  237. );
  238. }
  239. const StyledTextCopyInput = styled(TextCopyInput)`
  240. margin-top: ${space(0.5)};
  241. `;
  242. const NetworkUrlWrapper = styled('div')`
  243. margin: ${space(1)} ${space(0)} ${space(1.5)} ${space(0)};
  244. `;
  245. const NoMarginAlert = styled(Alert)`
  246. margin: 0;
  247. border-width: 1px 0 0 0;
  248. `;
  249. const StyledInstructions = styled('div')`
  250. font-size: ${p => p.theme.fontSizeSmall};
  251. margin-top: ${space(1)};
  252. border-top: 1px solid ${p => p.theme.border};
  253. padding: ${space(2)};
  254. &:first-child {
  255. margin-top: 0;
  256. border-top: none;
  257. }
  258. h1 {
  259. font-size: inherit;
  260. margin-bottom: ${space(1)};
  261. }
  262. p {
  263. margin-bottom: ${space(2)};
  264. }
  265. p:last-child {
  266. margin-bottom: 0;
  267. }
  268. `;