sections.tsx 6.8 KB

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