sections.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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. };
  112. }
  113. );
  114. return (
  115. <SectionItem title={t('Request Headers')}>
  116. {keyValueTableOrNotFound(headers, t('Headers not captured'))}
  117. </SectionItem>
  118. );
  119. }
  120. export function ResponseHeadersSection({item}: SectionProps) {
  121. const contentTypeHeaders = getReqRespContentTypes(item);
  122. const isContentTypeMismatched =
  123. contentTypeHeaders.req !== undefined &&
  124. contentTypeHeaders.resp !== undefined &&
  125. contentTypeHeaders.req !== contentTypeHeaders.resp;
  126. const data = isRequestFrame(item) ? item.data : {};
  127. const headers: KeyValueTuple[] = Object.entries(data.response?.headers || {}).map(
  128. ([key, value]) => {
  129. const warn = key === 'content-type' && isContentTypeMismatched;
  130. return {
  131. key,
  132. value: warn ? (
  133. <Flex align="center" gap={space(0.5)}>
  134. {value}
  135. <QuestionTooltip
  136. size="xs"
  137. title={t('The content-type of the request does not match the response.')}
  138. />
  139. </Flex>
  140. ) : (
  141. value
  142. ),
  143. type: warn ? 'warning' : undefined,
  144. tooltip: undefined,
  145. };
  146. }
  147. );
  148. return (
  149. <SectionItem title={t('Response Headers')}>
  150. {keyValueTableOrNotFound(headers, t('Headers not captured'))}
  151. </SectionItem>
  152. );
  153. }
  154. export function QueryParamsSection({item}: SectionProps) {
  155. const queryParams = queryString.parse(item.description?.split('?')?.[1] ?? '');
  156. return (
  157. <SectionItem title={t('Query String Parameters')}>
  158. <Indent>
  159. <ObjectInspector data={queryParams} expandLevel={3} showCopyButton />
  160. </Indent>
  161. </SectionItem>
  162. );
  163. }
  164. export function RequestPayloadSection({item}: SectionProps) {
  165. const {dismiss, isDismissed} = useDismissReqRespBodiesAlert();
  166. const data = useMemo(() => (isRequestFrame(item) ? item.data : {}), [item]);
  167. const {warnings, body} = getBodyAndWarnings(data.request);
  168. useEffect(() => {
  169. if (!isDismissed && 'request' in data) {
  170. dismiss();
  171. }
  172. }, [dismiss, data, isDismissed]);
  173. return (
  174. <SectionItem
  175. title={t('Request Body')}
  176. titleExtra={
  177. <SizeTooltip>
  178. {t('Size:')} {formatBytesBase10(data.request?.size ?? 0)}
  179. </SizeTooltip>
  180. }
  181. >
  182. <Indent>
  183. <Warning warnings={warnings} />
  184. {'request' in data ? (
  185. <ObjectInspector data={body} expandLevel={2} showCopyButton />
  186. ) : (
  187. t('Request body not found.')
  188. )}
  189. </Indent>
  190. </SectionItem>
  191. );
  192. }
  193. export function ResponsePayloadSection({item}: SectionProps) {
  194. const {dismiss, isDismissed} = useDismissReqRespBodiesAlert();
  195. const data = useMemo(() => (isRequestFrame(item) ? item.data : {}), [item]);
  196. const {warnings, body} = getBodyAndWarnings(data.response);
  197. useEffect(() => {
  198. if (!isDismissed && 'response' in data) {
  199. dismiss();
  200. }
  201. }, [dismiss, data, isDismissed]);
  202. return (
  203. <SectionItem
  204. title={t('Response Body')}
  205. titleExtra={
  206. <SizeTooltip>
  207. {t('Size:')} {formatBytesBase10(data.response?.size ?? 0)}
  208. </SizeTooltip>
  209. }
  210. >
  211. <Indent>
  212. <Warning warnings={warnings} />
  213. {'response' in data ? (
  214. <ObjectInspector data={body} expandLevel={2} showCopyButton />
  215. ) : (
  216. t('Response body not found.')
  217. )}
  218. </Indent>
  219. </SectionItem>
  220. );
  221. }
  222. function getBodyAndWarnings(reqOrRes?: ReplayNetworkRequestOrResponse): {
  223. body: ReplayNetworkRequestOrResponse['body'];
  224. warnings: NetworkMetaWarning[];
  225. } {
  226. if (!reqOrRes) {
  227. return {body: undefined, warnings: []};
  228. }
  229. const warnings = reqOrRes._meta?.warnings ?? [];
  230. let body = reqOrRes.body;
  231. if (typeof body === 'string' && warnings.includes('MAYBE_JSON_TRUNCATED')) {
  232. try {
  233. const json = fixJson(body);
  234. body = JSON.parse(json);
  235. warnings.push('JSON_TRUNCATED');
  236. } catch {
  237. // this can fail, in which case we just use the body string
  238. warnings.push('INVALID_JSON');
  239. warnings.push('TEXT_TRUNCATED');
  240. }
  241. }
  242. return {body, warnings};
  243. }