123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- 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: (
- <SizeTooltip>
- {formatBytesBase10(
- requestFrame?.data?.request?.size ?? requestFrame?.data?.requestBodySize ?? 0
- )}
- </SizeTooltip>
- ),
- },
- {
- key: t('Response Body Size'),
- value: (
- <SizeTooltip>
- {formatBytesBase10(
- requestFrame?.data?.response?.size ??
- requestFrame?.data?.responseBodySize ??
- 0
- )}
- </SizeTooltip>
- ),
- },
- {
- key: t('Duration'),
- value: `${(item.endTimestampMs - item.timestampMs).toFixed(2)}ms`,
- },
- {
- key: t('Timestamp'),
- value: (
- <TimestampButton
- format="mm:ss.SSS"
- onClick={(event: MouseEvent) => {
- event.stopPropagation();
- setCurrentTime(item.offsetMs);
- }}
- startTimestampMs={startTimestampMs}
- timestampMs={item.timestampMs}
- />
- ),
- },
- ];
- return (
- <SectionItem title={t('General')}>
- {keyValueTableOrNotFound(data, t('Missing request details'))}
- </SectionItem>
- );
- }
- 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 ? (
- <Flex align="center" gap={space(0.5)}>
- {value}
- <QuestionTooltip
- size="xs"
- title={t('The content-type of the request does not match the response.')}
- />
- </Flex>
- ) : (
- value
- ),
- type: warn ? 'warning' : undefined,
- };
- }
- );
- return (
- <SectionItem title={t('Request Headers')}>
- {keyValueTableOrNotFound(headers, t('Headers not captured'))}
- </SectionItem>
- );
- }
- 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 ? (
- <Flex align="center" gap={space(0.5)}>
- {value}
- <QuestionTooltip
- size="xs"
- title={t('The content-type of the request does not match the response.')}
- />
- </Flex>
- ) : (
- value
- ),
- type: warn ? 'warning' : undefined,
- tooltip: undefined,
- };
- }
- );
- return (
- <SectionItem title={t('Response Headers')}>
- {keyValueTableOrNotFound(headers, t('Headers not captured'))}
- </SectionItem>
- );
- }
- export function QueryParamsSection({item}: SectionProps) {
- const queryParams = queryString.parse(item.description?.split('?')?.[1] ?? '');
- return (
- <SectionItem title={t('Query String Parameters')}>
- <Indent>
- <ObjectInspector data={queryParams} expandLevel={3} showCopyButton />
- </Indent>
- </SectionItem>
- );
- }
- 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 (
- <SectionItem
- title={t('Request Body')}
- titleExtra={
- <SizeTooltip>
- {t('Size:')} {formatBytesBase10(data.request?.size ?? 0)}
- </SizeTooltip>
- }
- >
- <Indent>
- <Warning warnings={warnings} />
- {'request' in data ? (
- <ObjectInspector data={body} expandLevel={2} showCopyButton />
- ) : (
- t('Request body not found.')
- )}
- </Indent>
- </SectionItem>
- );
- }
- 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 (
- <SectionItem
- title={t('Response Body')}
- titleExtra={
- <SizeTooltip>
- {t('Size:')} {formatBytesBase10(data.response?.size ?? 0)}
- </SizeTooltip>
- }
- >
- <Indent>
- <Warning warnings={warnings} />
- {'response' in data ? (
- <ObjectInspector data={body} expandLevel={2} showCopyButton />
- ) : (
- t('Response body not found.')
- )}
- </Indent>
- </SectionItem>
- );
- }
- 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};
- }
|