sections.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import type {MouseEvent} from 'react';
  2. import {useEffect, useMemo} from 'react';
  3. import queryString from 'query-string';
  4. import ObjectInspector from 'sentry/components/objectInspector';
  5. import {Flex} from 'sentry/components/profiling/flex';
  6. import QuestionTooltip from 'sentry/components/questionTooltip';
  7. import {useReplayContext} from 'sentry/components/replays/replayContext';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {formatBytesBase10} from 'sentry/utils';
  11. import type {
  12. NetworkMetaWarning,
  13. ReplayNetworkRequestOrResponse,
  14. } from 'sentry/utils/replays/replay';
  15. import {
  16. getFrameMethod,
  17. getFrameStatus,
  18. getReqRespContentTypes,
  19. isRequestFrame,
  20. } from 'sentry/utils/replays/resourceFrame';
  21. import type {SpanFrame} from 'sentry/utils/replays/types';
  22. import type {KeyValueTuple} from 'sentry/views/replays/detail/network/details/components';
  23. import {
  24. Indent,
  25. keyValueTableOrNotFound,
  26. SectionItem,
  27. SizeTooltip,
  28. Warning,
  29. } from 'sentry/views/replays/detail/network/details/components';
  30. import {useDismissReqRespBodiesAlert} from 'sentry/views/replays/detail/network/details/onboarding';
  31. import {fixJson} from 'sentry/views/replays/detail/network/truncateJson/fixJson';
  32. import TimestampButton from 'sentry/views/replays/detail/timestampButton';
  33. export type SectionProps = {
  34. item: SpanFrame;
  35. projectId: string;
  36. startTimestampMs: number;
  37. };
  38. const UNKNOWN_STATUS = 'unknown';
  39. export function GeneralSection({item, startTimestampMs}: SectionProps) {
  40. const {setCurrentTime} = useReplayContext();
  41. const requestFrame = isRequestFrame(item) ? item : null;
  42. const data: KeyValueTuple[] = [
  43. {key: t('URL'), value: item.description},
  44. {key: t('Type'), value: item.op},
  45. {key: t('Method'), value: getFrameMethod(item)},
  46. {key: t('Status Code'), value: String(getFrameStatus(item) ?? UNKNOWN_STATUS)},
  47. {
  48. key: t('Request Body Size'),
  49. value: (
  50. <SizeTooltip>
  51. {formatBytesBase10(
  52. requestFrame?.data?.request?.size ?? requestFrame?.data?.requestBodySize ?? 0
  53. )}
  54. </SizeTooltip>
  55. ),
  56. },
  57. {
  58. key: t('Response Body Size'),
  59. value: (
  60. <SizeTooltip>
  61. {formatBytesBase10(
  62. requestFrame?.data?.response?.size ??
  63. requestFrame?.data?.responseBodySize ??
  64. 0
  65. )}
  66. </SizeTooltip>
  67. ),
  68. },
  69. {
  70. key: t('Duration'),
  71. value: `${(item.endTimestampMs - item.timestampMs).toFixed(2)}ms`,
  72. },
  73. {
  74. key: t('Timestamp'),
  75. value: (
  76. <TimestampButton
  77. format="mm:ss.SSS"
  78. onClick={(event: MouseEvent) => {
  79. event.stopPropagation();
  80. setCurrentTime(item.offsetMs);
  81. }}
  82. startTimestampMs={startTimestampMs}
  83. timestampMs={item.timestampMs}
  84. />
  85. ),
  86. },
  87. ];
  88. return (
  89. <SectionItem title={t('General')}>
  90. {keyValueTableOrNotFound(data, t('Missing request details'))}
  91. </SectionItem>
  92. );
  93. }
  94. export function RequestHeadersSection({item}: SectionProps) {
  95. const contentTypeHeaders = getReqRespContentTypes(item);
  96. const isContentTypeMismatched =
  97. contentTypeHeaders.req !== undefined &&
  98. contentTypeHeaders.resp !== undefined &&
  99. contentTypeHeaders.req !== contentTypeHeaders.resp;
  100. const data = isRequestFrame(item) ? item.data : {};
  101. const headers: KeyValueTuple[] = Object.entries(data.request?.headers || {}).map(
  102. ([key, value]) => {
  103. const warn = key === 'content-type' && isContentTypeMismatched;
  104. return {
  105. key,
  106. value: warn ? (
  107. <Flex align="center" gap={space(0.5)}>
  108. {value}
  109. <QuestionTooltip
  110. size="xs"
  111. title={t('The content-type of the request does not match the response.')}
  112. />
  113. </Flex>
  114. ) : (
  115. value
  116. ),
  117. type: warn ? 'warning' : undefined,
  118. };
  119. }
  120. );
  121. return (
  122. <SectionItem title={t('Request Headers')}>
  123. {keyValueTableOrNotFound(headers, t('Headers not captured'))}
  124. </SectionItem>
  125. );
  126. }
  127. export function ResponseHeadersSection({item}: SectionProps) {
  128. const contentTypeHeaders = getReqRespContentTypes(item);
  129. const isContentTypeMismatched =
  130. contentTypeHeaders.req !== undefined &&
  131. contentTypeHeaders.resp !== undefined &&
  132. contentTypeHeaders.req !== contentTypeHeaders.resp;
  133. const data = isRequestFrame(item) ? item.data : {};
  134. const headers: KeyValueTuple[] = Object.entries(data.response?.headers || {}).map(
  135. ([key, value]) => {
  136. const warn = key === 'content-type' && isContentTypeMismatched;
  137. return {
  138. key,
  139. value: warn ? (
  140. <Flex align="center" gap={space(0.5)}>
  141. {value}
  142. <QuestionTooltip
  143. size="xs"
  144. title={t('The content-type of the request does not match the response.')}
  145. />
  146. </Flex>
  147. ) : (
  148. value
  149. ),
  150. type: warn ? 'warning' : undefined,
  151. tooltip: undefined,
  152. };
  153. }
  154. );
  155. return (
  156. <SectionItem title={t('Response Headers')}>
  157. {keyValueTableOrNotFound(headers, t('Headers not captured'))}
  158. </SectionItem>
  159. );
  160. }
  161. export function QueryParamsSection({item}: SectionProps) {
  162. const queryParams = queryString.parse(item.description?.split('?')?.[1] ?? '');
  163. return (
  164. <SectionItem title={t('Query String Parameters')}>
  165. <Indent>
  166. <ObjectInspector data={queryParams} expandLevel={3} showCopyButton />
  167. </Indent>
  168. </SectionItem>
  169. );
  170. }
  171. export function RequestPayloadSection({item}: SectionProps) {
  172. const {dismiss, isDismissed} = useDismissReqRespBodiesAlert();
  173. const data = useMemo(() => (isRequestFrame(item) ? item.data : {}), [item]);
  174. const {warnings, body} = getBodyAndWarnings(data.request);
  175. useEffect(() => {
  176. if (!isDismissed && 'request' in data) {
  177. dismiss();
  178. }
  179. }, [dismiss, data, isDismissed]);
  180. return (
  181. <SectionItem
  182. title={t('Request Body')}
  183. titleExtra={
  184. <SizeTooltip>
  185. {t('Size:')} {formatBytesBase10(data.request?.size ?? 0)}
  186. </SizeTooltip>
  187. }
  188. >
  189. <Indent>
  190. <Warning warnings={warnings} />
  191. {'request' in data ? (
  192. <ObjectInspector data={body} expandLevel={2} showCopyButton />
  193. ) : (
  194. t('Request body not found.')
  195. )}
  196. </Indent>
  197. </SectionItem>
  198. );
  199. }
  200. export function ResponsePayloadSection({item}: SectionProps) {
  201. const {dismiss, isDismissed} = useDismissReqRespBodiesAlert();
  202. const data = useMemo(() => (isRequestFrame(item) ? item.data : {}), [item]);
  203. const {warnings, body} = getBodyAndWarnings(data.response);
  204. useEffect(() => {
  205. if (!isDismissed && 'response' in data) {
  206. dismiss();
  207. }
  208. }, [dismiss, data, isDismissed]);
  209. return (
  210. <SectionItem
  211. title={t('Response Body')}
  212. titleExtra={
  213. <SizeTooltip>
  214. {t('Size:')} {formatBytesBase10(data.response?.size ?? 0)}
  215. </SizeTooltip>
  216. }
  217. >
  218. <Indent>
  219. <Warning warnings={warnings} />
  220. {'response' in data ? (
  221. <ObjectInspector data={body} expandLevel={2} showCopyButton />
  222. ) : (
  223. t('Response body not found.')
  224. )}
  225. </Indent>
  226. </SectionItem>
  227. );
  228. }
  229. function getBodyAndWarnings(reqOrRes?: ReplayNetworkRequestOrResponse): {
  230. body: ReplayNetworkRequestOrResponse['body'];
  231. warnings: NetworkMetaWarning[];
  232. } {
  233. if (!reqOrRes) {
  234. return {body: undefined, warnings: []};
  235. }
  236. const warnings = reqOrRes._meta?.warnings ?? [];
  237. let body = reqOrRes.body;
  238. if (typeof body === 'string' && warnings.includes('MAYBE_JSON_TRUNCATED')) {
  239. try {
  240. const json = fixJson(body);
  241. body = JSON.parse(json);
  242. warnings.push('JSON_TRUNCATED');
  243. } catch {
  244. // this can fail, in which case we just use the body string
  245. warnings.push('INVALID_JSON');
  246. warnings.push('TEXT_TRUNCATED');
  247. }
  248. }
  249. return {body, warnings};
  250. }