import type {MouseEvent} from 'react'; import {useEffect, useMemo} from 'react'; import queryString from 'query-string'; import ObjectInspector from 'sentry/components/objectInspector'; import {Flex} from 'sentry/components/profiling/flex'; import QuestionTooltip from 'sentry/components/questionTooltip'; import {useReplayContext} from 'sentry/components/replays/replayContext'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {formatBytesBase10} from 'sentry/utils'; import type { NetworkMetaWarning, ReplayNetworkRequestOrResponse, } from 'sentry/utils/replays/replay'; import { getFrameMethod, getFrameStatus, getReqRespContentTypes, isRequestFrame, } from 'sentry/utils/replays/resourceFrame'; import type {SpanFrame} from 'sentry/utils/replays/types'; import type {KeyValueTuple} from 'sentry/views/replays/detail/network/details/components'; import { Indent, keyValueTableOrNotFound, SectionItem, SizeTooltip, Warning, } from 'sentry/views/replays/detail/network/details/components'; import {useDismissReqRespBodiesAlert} from 'sentry/views/replays/detail/network/details/onboarding'; import {fixJson} from 'sentry/views/replays/detail/network/truncateJson/fixJson'; import TimestampButton from 'sentry/views/replays/detail/timestampButton'; export type SectionProps = { item: SpanFrame; projectId: string; startTimestampMs: number; }; const UNKNOWN_STATUS = 'unknown'; export function GeneralSection({item, startTimestampMs}: SectionProps) { const {setCurrentTime} = useReplayContext(); const requestFrame = isRequestFrame(item) ? item : null; const data: KeyValueTuple[] = [ {key: t('URL'), value: item.description}, {key: t('Type'), value: item.op}, {key: t('Method'), value: getFrameMethod(item)}, {key: t('Status Code'), value: String(getFrameStatus(item) ?? UNKNOWN_STATUS)}, { key: t('Request Body Size'), value: ( {formatBytesBase10(requestFrame?.data?.request?.size ?? 0)} ), }, { key: t('Response Body Size'), value: ( {formatBytesBase10(requestFrame?.data?.response?.size ?? 0)} ), }, { key: t('Duration'), value: `${(item.endTimestampMs - item.timestampMs).toFixed(2)}ms`, }, { key: t('Timestamp'), value: ( { event.stopPropagation(); setCurrentTime(item.offsetMs); }} startTimestampMs={startTimestampMs} timestampMs={item.timestampMs} /> ), }, ]; return ( {keyValueTableOrNotFound(data, t('Missing request details'))} ); } export function RequestHeadersSection({item}: SectionProps) { const contentTypeHeaders = getReqRespContentTypes(item); const isContentTypeMismatched = contentTypeHeaders.req !== undefined && contentTypeHeaders.resp !== undefined && contentTypeHeaders.req !== contentTypeHeaders.resp; const data = isRequestFrame(item) ? item.data : {}; const headers: KeyValueTuple[] = Object.entries(data.request?.headers || {}).map( ([key, value]) => { const warn = key === 'content-type' && isContentTypeMismatched; return { key, value: warn ? ( {value} ) : ( value ), type: warn ? 'warning' : undefined, }; } ); return ( {keyValueTableOrNotFound(headers, t('Headers not captured'))} ); } export function ResponseHeadersSection({item}: SectionProps) { const contentTypeHeaders = getReqRespContentTypes(item); const isContentTypeMismatched = contentTypeHeaders.req !== undefined && contentTypeHeaders.resp !== undefined && contentTypeHeaders.req !== contentTypeHeaders.resp; const data = isRequestFrame(item) ? item.data : {}; const headers: KeyValueTuple[] = Object.entries(data.response?.headers || {}).map( ([key, value]) => { const warn = key === 'content-type' && isContentTypeMismatched; return { key, value: warn ? ( {value} ) : ( value ), type: warn ? 'warning' : undefined, tooltip: undefined, }; } ); return ( {keyValueTableOrNotFound(headers, t('Headers not captured'))} ); } export function QueryParamsSection({item}: SectionProps) { const queryParams = queryString.parse(item.description?.split('?')?.[1] ?? ''); return ( ); } export function RequestPayloadSection({item}: SectionProps) { const {dismiss, isDismissed} = useDismissReqRespBodiesAlert(); const data = useMemo(() => (isRequestFrame(item) ? item.data : {}), [item]); const {warnings, body} = getBodyAndWarnings(data.request); useEffect(() => { if (!isDismissed && 'request' in data) { dismiss(); } }, [dismiss, data, isDismissed]); return ( {t('Size:')} {formatBytesBase10(data.request?.size ?? 0)} } > {'request' in data ? ( ) : ( t('Request body not found.') )} ); } export function ResponsePayloadSection({item}: SectionProps) { const {dismiss, isDismissed} = useDismissReqRespBodiesAlert(); const data = useMemo(() => (isRequestFrame(item) ? item.data : {}), [item]); const {warnings, body} = getBodyAndWarnings(data.response); useEffect(() => { if (!isDismissed && 'response' in data) { dismiss(); } }, [dismiss, data, isDismissed]); return ( {t('Size:')} {formatBytesBase10(data.response?.size ?? 0)} } > {'response' in data ? ( ) : ( t('Response body not found.') )} ); } function getBodyAndWarnings(reqOrRes?: ReplayNetworkRequestOrResponse): { body: ReplayNetworkRequestOrResponse['body']; warnings: NetworkMetaWarning[]; } { if (!reqOrRes) { return {body: undefined, warnings: []}; } const warnings = reqOrRes._meta?.warnings ?? []; let body = reqOrRes.body; if (typeof body === 'string' && warnings.includes('MAYBE_JSON_TRUNCATED')) { try { const json = fixJson(body); body = JSON.parse(json); warnings.push('JSON_TRUNCATED'); } catch { // this can fail, in which case we just use the body string warnings.push('INVALID_JSON'); warnings.push('TEXT_TRUNCATED'); } } return {body, warnings}; }