onboarding.tsx 7.5 KB

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