sections.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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(requestFrame?.data?.request?.size ?? 0)}
  52. </SizeTooltip>
  53. ),
  54. },
  55. {
  56. key: t('Response Body Size'),
  57. value: (
  58. <SizeTooltip>
  59. {formatBytesBase10(requestFrame?.data?.response?.size ?? 0)}
  60. </SizeTooltip>
  61. ),
  62. },
  63. {
  64. key: t('Duration'),
  65. value: `${(item.endTimestampMs - item.timestampMs).toFixed(2)}ms`,
  66. },
  67. {
  68. key: t('Timestamp'),
  69. value: (
  70. <TimestampButton
  71. format="mm:ss.SSS"
  72. onClick={(event: MouseEvent) => {
  73. event.stopPropagation();
  74. setCurrentTime(item.offsetMs);
  75. }}
  76. startTimestampMs={startTimestampMs}
  77. timestampMs={item.timestampMs}
  78. />
  79. ),
  80. },
  81. ];
  82. return (
  83. <SectionItem title={t('General')}>
  84. {keyValueTableOrNotFound(data, t('Missing request details'))}
  85. </SectionItem>
  86. );
  87. }
  88. export function RequestHeadersSection({item}: SectionProps) {
  89. const contentTypeHeaders = getReqRespContentTypes(item);
  90. const isContentTypeMismatched =
  91. contentTypeHeaders.req !== undefined &&
  92. contentTypeHeaders.resp !== undefined &&
  93. contentTypeHeaders.req !== contentTypeHeaders.resp;
  94. const data = isRequestFrame(item) ? item.data : {};
  95. const headers: KeyValueTuple[] = Object.entries(data.request?.headers || {}).map(
  96. ([key, value]) => {
  97. const warn = key === 'content-type' && isContentTypeMismatched;
  98. return {
  99. key,
  100. value: warn ? (
  101. <Flex align="center" gap={space(0.5)}>
  102. {value}
  103. <QuestionTooltip
  104. size="xs"
  105. title={t('The content-type of the request does not match the response.')}
  106. />
  107. </Flex>
  108. ) : (
  109. value
  110. ),
  111. type: warn ? 'warning' : 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. }