import styled from '@emotion/styled'; import Alert from 'sentry/components/alert'; import {Button} from 'sentry/components/button'; import {CodeSnippet} from 'sentry/components/codeSnippet'; import ExternalLink from 'sentry/components/links/externalLink'; import TextCopyInput from 'sentry/components/textCopyInput'; import {IconClose, IconInfo} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {SpanFrame} from 'sentry/utils/replays/types'; import useDismissAlert from 'sentry/utils/useDismissAlert'; import useOrganization from 'sentry/utils/useOrganization'; import useProjectSdkNeedsUpdate from 'sentry/utils/useProjectSdkNeedsUpdate'; import {Output} from 'sentry/views/replays/detail/network/details/getOutputType'; import type {TabKey} from 'sentry/views/replays/detail/network/details/tabs'; export const useDismissReqRespBodiesAlert = () => { const organization = useOrganization(); return useDismissAlert({ key: `${organization.id}:replay-network-bodies-alert-dismissed`, }); }; export function ReqRespBodiesAlert({ isNetworkDetailsSetup, }: { isNetworkDetailsSetup: boolean; }) { const {dismiss, isDismissed} = useDismissReqRespBodiesAlert(); if (isDismissed) { return null; } const message = isNetworkDetailsSetup ? tct( 'Click on a [fetch] or [xhr] request to see request and response bodies. [link].', { fetch: fetch, xhr: xhr, link: ( {t('Learn More')} ), } ) : tct('Start collecting the body of requests and responses. [link].', { link: ( {t('Learn More')} ), }); return ( } opaque={false} showIcon type="info" trailingItems={ } > {message} ); } const StyledAlert = styled(Alert)` margin-bottom: ${space(1)}; `; const StyledButton = styled(Button)` color: inherit; `; export function UnsupportedOp({type}: {type: 'headers' | 'bodies'}) { const title = type === 'bodies' ? t('Capture Request and Response Bodies') : t('Capture Request and Response Headers'); return (

{title}

{tct( `This feature is only compatible with [fetch] and [xhr] request types. [link].`, { fetch: fetch, xhr: xhr, link: ( {t('Learn more')} ), } )}

); } export function Setup({ item, projectId, showSnippet, visibleTab, }: { item: SpanFrame; projectId: string; showSnippet: Output; visibleTab: TabKey; }) { const organization = useOrganization(); const {isFetching, needsUpdate} = useProjectSdkNeedsUpdate({ // Only show update instructions if not >= 7.50.0, but our instructions // will show a different min version as there are known bugs in 7.50 -> // 7.53 minVersion: '7.50.0', organization, projectId: [projectId], }); const sdkNeedsUpdate = !isFetching && Boolean(needsUpdate); const url = item.description || 'http://example.com'; return ( ); } function SetupInstructions({ minVersion, sdkNeedsUpdate, showSnippet, url, visibleTab, }: { minVersion: string; sdkNeedsUpdate: boolean; showSnippet: Output; url: string; visibleTab: TabKey; }) { if (showSnippet === Output.DATA && visibleTab === 'details') { return ( {tct( 'You can capture additional headers by adding them to the [requestConfig] and [responseConfig] lists in your SDK config.', { requestConfig: networkRequestHeaders, responseConfig: networkResponseHeaders, } )} ); } function trimUrl(oldUrl: string): string { const end = oldUrl.indexOf('?') > 0 ? oldUrl.indexOf('?') : oldUrl.length; return oldUrl.substring(0, end); } const urlSnippet = ` networkDetailAllowUrls: ['${trimUrl(url)}'],`; const headersSnippet = ` networkRequestHeaders: ['X-Custom-Header'], networkResponseHeaders: ['X-Custom-Header'],`; const includeHeadersSnippet = showSnippet === Output.SETUP || ([Output.URL_SKIPPED, Output.DATA].includes(showSnippet) && visibleTab === 'details'); const code = `Sentry.init({ integrations: [ new Replay({${urlSnippet + (includeHeadersSnippet ? headersSnippet : '')} }), ], })`; const title = showSnippet === Output.SETUP ? t('Capture Request and Response Headers and Bodies') : visibleTab === 'details' ? t('Capture Request and Response Headers') : t('Capture Request and Response Bodies'); return (

{title}

{tct( `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].`, { link: ( {t('Learn More')} ), } )}

{showSnippet === Output.URL_SKIPPED && url !== '[Filtered]' && tct( 'Add the following to your [field] list to start capturing data: [alert] ', { field: networkDetailAllowUrls, alert: {trimUrl(url)}, } )} {showSnippet === Output.BODY_SKIPPED && ( {tct('Enable [field] to capture both Request and Response bodies.', { field: networkCaptureBodies: true, })} )}

{t('Prerequisites')}

    {sdkNeedsUpdate ? (
  1. {tct('Update your SDK version to >= [minVersion]', { minVersion, })}
  2. ) : null}
  3. {t('Edit the Replay integration configuration to allow this URL.')}
  4. {t('That’s it!')}
{url !== '[Filtered]' && ( {code} )}
); } const StyledTextCopyInput = styled(TextCopyInput)` margin-top: ${space(0.5)}; `; const NetworkUrlWrapper = styled('div')` margin: ${space(1)} ${space(0)} ${space(1.5)} ${space(0)}; `; const NoMarginAlert = styled(Alert)` margin: 0; border-width: 1px 0 0 0; `; const StyledInstructions = styled('div')` font-size: ${p => p.theme.fontSizeSmall}; margin-top: ${space(1)}; border-top: 1px solid ${p => p.theme.border}; padding: ${space(2)}; &:first-child { margin-top: 0; border-top: none; } h1 { font-size: inherit; margin-bottom: ${space(1)}; } p { margin-bottom: ${space(2)}; } p:last-child { margin-bottom: 0; } `;