sections.tsx 7.6 KB

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