onboarding.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import styled from '@emotion/styled';
  2. import Alert from 'sentry/components/alert';
  3. import {CodeSnippet} from 'sentry/components/codeSnippet';
  4. import ExternalLink from 'sentry/components/links/externalLink';
  5. import TextCopyInput from 'sentry/components/textCopyInput';
  6. import {t, tct} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import type {SpanFrame} from 'sentry/utils/replays/types';
  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. export const useDismissReqRespBodiesAlert = () => {
  15. const organization = useOrganization();
  16. return useDismissAlert({
  17. key: `${organization.id}:replay-network-bodies-alert-dismissed`,
  18. });
  19. };
  20. export function UnsupportedOp({type}: {type: 'headers' | 'bodies'}) {
  21. const title =
  22. type === 'bodies'
  23. ? t('Capture Request and Response Bodies')
  24. : t('Capture Request and Response Headers');
  25. return (
  26. <StyledInstructions data-test-id="network-op-unsupported">
  27. <h1>{title}</h1>
  28. <p>
  29. {tct(
  30. `This feature is only compatible with [fetch] and [xhr] request types. [link].`,
  31. {
  32. fetch: <code>fetch</code>,
  33. xhr: <code>xhr</code>,
  34. link: (
  35. <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details">
  36. {t('Learn more')}
  37. </ExternalLink>
  38. ),
  39. }
  40. )}
  41. </p>
  42. </StyledInstructions>
  43. );
  44. }
  45. export function Setup({
  46. item,
  47. projectId,
  48. showSnippet,
  49. visibleTab,
  50. }: {
  51. item: SpanFrame;
  52. projectId: string;
  53. showSnippet: Output;
  54. visibleTab: TabKey;
  55. }) {
  56. const organization = useOrganization();
  57. const {isFetching, needsUpdate} = useProjectSdkNeedsUpdate({
  58. // Only show update instructions if not >= 7.50.0, but our instructions
  59. // will show a different min version as there are known bugs in 7.50 ->
  60. // 7.53
  61. minVersion: '7.50.0',
  62. organization,
  63. projectId: [projectId],
  64. });
  65. const sdkNeedsUpdate = !isFetching && Boolean(needsUpdate);
  66. const url = item.description || 'http://example.com';
  67. return (
  68. <SetupInstructions
  69. minVersion="7.53.1"
  70. sdkNeedsUpdate={sdkNeedsUpdate}
  71. showSnippet={showSnippet}
  72. url={url}
  73. visibleTab={visibleTab}
  74. />
  75. );
  76. }
  77. function SetupInstructions({
  78. minVersion,
  79. sdkNeedsUpdate,
  80. showSnippet,
  81. url,
  82. visibleTab,
  83. }: {
  84. minVersion: string;
  85. sdkNeedsUpdate: boolean;
  86. showSnippet: Output;
  87. url: string;
  88. visibleTab: TabKey;
  89. }) {
  90. if (showSnippet === Output.DATA && visibleTab === 'details') {
  91. return (
  92. <NoMarginAlert type="muted" system data-test-id="network-setup-steps">
  93. {tct(
  94. 'You can capture additional headers by adding them to the [requestConfig] and [responseConfig] lists in your SDK config.',
  95. {
  96. requestConfig: <code>networkRequestHeaders</code>,
  97. responseConfig: <code>networkResponseHeaders</code>,
  98. }
  99. )}
  100. </NoMarginAlert>
  101. );
  102. }
  103. function trimUrl(oldUrl: string): string {
  104. const end = oldUrl.indexOf('?') > 0 ? oldUrl.indexOf('?') : oldUrl.length;
  105. return oldUrl.substring(0, end);
  106. }
  107. const urlSnippet = `
  108. networkDetailAllowUrls: ['${trimUrl(url)}'],`;
  109. const headersSnippet = `
  110. networkRequestHeaders: ['X-Custom-Header'],
  111. networkResponseHeaders: ['X-Custom-Header'],`;
  112. const includeHeadersSnippet =
  113. showSnippet === Output.SETUP ||
  114. ([Output.URL_SKIPPED, Output.DATA].includes(showSnippet) && visibleTab === 'details');
  115. const code = `Sentry.init({
  116. integrations: [
  117. new Replay({${urlSnippet + (includeHeadersSnippet ? headersSnippet : '')}
  118. }),
  119. ],
  120. })`;
  121. const title =
  122. showSnippet === Output.SETUP
  123. ? t('Capture Request and Response Headers and Bodies')
  124. : visibleTab === 'details'
  125. ? t('Capture Request and Response Headers')
  126. : t('Capture Request and Response Bodies');
  127. return (
  128. <StyledInstructions data-test-id="network-setup-steps">
  129. <h1>{title}</h1>
  130. <p>
  131. {tct(
  132. `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].`,
  133. {
  134. link: (
  135. <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/configuration/#network-details">
  136. {t('Learn More')}
  137. </ExternalLink>
  138. ),
  139. }
  140. )}
  141. </p>
  142. <NetworkUrlWrapper>
  143. {showSnippet === Output.URL_SKIPPED &&
  144. url !== '[Filtered]' &&
  145. tct(
  146. 'Add the following to your [field] list to start capturing data: [alert] ',
  147. {
  148. field: <code>networkDetailAllowUrls</code>,
  149. alert: <StyledTextCopyInput>{trimUrl(url)}</StyledTextCopyInput>,
  150. }
  151. )}
  152. </NetworkUrlWrapper>
  153. {showSnippet === Output.BODY_SKIPPED && (
  154. <Alert type="warning">
  155. {tct('Enable [field] to capture both Request and Response bodies.', {
  156. field: <code>networkCaptureBodies: true</code>,
  157. })}
  158. </Alert>
  159. )}
  160. <h1>{t('Prerequisites')}</h1>
  161. <ol>
  162. {sdkNeedsUpdate ? (
  163. <li>
  164. {tct('Update your SDK version to >= [minVersion]', {
  165. minVersion,
  166. })}
  167. </li>
  168. ) : null}
  169. <li>{t('Edit the Replay integration configuration to allow this URL.')}</li>
  170. <li>{t('That’s it!')}</li>
  171. </ol>
  172. {url !== '[Filtered]' && (
  173. <CodeSnippet filename="JavaScript" language="javascript">
  174. {code}
  175. </CodeSnippet>
  176. )}
  177. </StyledInstructions>
  178. );
  179. }
  180. const StyledTextCopyInput = styled(TextCopyInput)`
  181. margin-top: ${space(0.5)};
  182. `;
  183. const NetworkUrlWrapper = styled('div')`
  184. margin: ${space(1)} ${space(0)} ${space(1.5)} ${space(0)};
  185. `;
  186. const NoMarginAlert = styled(Alert)`
  187. margin: 0;
  188. border-width: 1px 0 0 0;
  189. `;
  190. const StyledInstructions = styled('div')`
  191. font-size: ${p => p.theme.fontSizeSmall};
  192. margin-top: ${space(1)};
  193. border-top: 1px solid ${p => p.theme.border};
  194. padding: ${space(2)};
  195. &:first-child {
  196. margin-top: 0;
  197. border-top: none;
  198. }
  199. h1 {
  200. font-size: inherit;
  201. margin-bottom: ${space(1)};
  202. }
  203. p {
  204. margin-bottom: ${space(2)};
  205. }
  206. p:last-child {
  207. margin-bottom: 0;
  208. }
  209. `;