sections.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import {MouseEvent, useEffect, useMemo} from 'react';
  2. import queryString from 'query-string';
  3. import ObjectInspector from 'sentry/components/objectInspector';
  4. import {useReplayContext} from 'sentry/components/replays/replayContext';
  5. import {t} from 'sentry/locale';
  6. import {formatBytesBase10} from 'sentry/utils';
  7. import {
  8. getFrameMethod,
  9. getFrameStatus,
  10. isRequestFrame,
  11. } from 'sentry/utils/replays/resourceFrame';
  12. import type {SpanFrame} from 'sentry/utils/replays/types';
  13. import {
  14. Indent,
  15. keyValueTableOrNotFound,
  16. SectionItem,
  17. SizeTooltip,
  18. Warning,
  19. } from 'sentry/views/replays/detail/network/details/components';
  20. import {useDismissReqRespBodiesAlert} from 'sentry/views/replays/detail/network/details/onboarding';
  21. import TimestampButton from 'sentry/views/replays/detail/timestampButton';
  22. export type SectionProps = {
  23. item: SpanFrame;
  24. projectId: string;
  25. startTimestampMs: number;
  26. };
  27. const UNKNOWN_STATUS = 'unknown';
  28. export function GeneralSection({item, startTimestampMs}: SectionProps) {
  29. const {setCurrentTime} = useReplayContext();
  30. const requestFrame = isRequestFrame(item) ? item : null;
  31. // TODO[replay]: what about:
  32. // `requestFrame?.data?.request?.size` vs. `requestFrame?.data?.requestBodySize`
  33. const data = {
  34. [t('URL')]: item.description,
  35. [t('Type')]: item.op,
  36. [t('Method')]: getFrameMethod(item),
  37. [t('Status Code')]: String(getFrameStatus(item) ?? UNKNOWN_STATUS),
  38. [t('Request Body Size')]: (
  39. <SizeTooltip>
  40. {formatBytesBase10(requestFrame?.data?.request?.size ?? 0)}
  41. </SizeTooltip>
  42. ),
  43. [t('Response Body Size')]: (
  44. <SizeTooltip>
  45. {formatBytesBase10(requestFrame?.data?.response?.size ?? 0)}
  46. </SizeTooltip>
  47. ),
  48. [t('Duration')]: `${(item.endTimestampMs - item.timestampMs).toFixed(2)}ms`,
  49. [t('Timestamp')]: (
  50. <TimestampButton
  51. format="mm:ss.SSS"
  52. onClick={(event: MouseEvent) => {
  53. event.stopPropagation();
  54. setCurrentTime(item.offsetMs);
  55. }}
  56. startTimestampMs={startTimestampMs}
  57. timestampMs={item.timestampMs}
  58. />
  59. ),
  60. };
  61. return (
  62. <SectionItem title={t('General')}>
  63. {keyValueTableOrNotFound(data, t('Missing request details'))}
  64. </SectionItem>
  65. );
  66. }
  67. export function RequestHeadersSection({item}: SectionProps) {
  68. const data = isRequestFrame(item) ? item.data : {};
  69. return (
  70. <SectionItem title={t('Request Headers')}>
  71. {keyValueTableOrNotFound(data.request?.headers, t('Headers not captured'))}
  72. </SectionItem>
  73. );
  74. }
  75. export function ResponseHeadersSection({item}: SectionProps) {
  76. const data = isRequestFrame(item) ? item.data : {};
  77. return (
  78. <SectionItem title={t('Response Headers')}>
  79. {keyValueTableOrNotFound(data.response?.headers, t('Headers not captured'))}
  80. </SectionItem>
  81. );
  82. }
  83. export function QueryParamsSection({item}: SectionProps) {
  84. const queryParams = queryString.parse(item.description?.split('?')?.[1] ?? '');
  85. return (
  86. <SectionItem title={t('Query String Parameters')}>
  87. <Indent>
  88. <ObjectInspector data={queryParams} expandLevel={3} showCopyButton />
  89. </Indent>
  90. </SectionItem>
  91. );
  92. }
  93. export function RequestPayloadSection({item}: SectionProps) {
  94. const {dismiss, isDismissed} = useDismissReqRespBodiesAlert();
  95. const data = useMemo(() => (isRequestFrame(item) ? item.data : {}), [item]);
  96. useEffect(() => {
  97. if (!isDismissed && 'request' in data) {
  98. dismiss();
  99. }
  100. }, [dismiss, data, isDismissed]);
  101. return (
  102. <SectionItem
  103. title={t('Request Body')}
  104. titleExtra={
  105. <SizeTooltip>
  106. {t('Size:')} {formatBytesBase10(data.request?.size ?? 0)}
  107. </SizeTooltip>
  108. }
  109. >
  110. <Indent>
  111. <Warning warnings={data.request?._meta?.warnings} />
  112. {'request' in data ? (
  113. <ObjectInspector data={data.request?.body} expandLevel={2} showCopyButton />
  114. ) : (
  115. t('Request body not found.')
  116. )}
  117. </Indent>
  118. </SectionItem>
  119. );
  120. }
  121. export function ResponsePayloadSection({item}: SectionProps) {
  122. const {dismiss, isDismissed} = useDismissReqRespBodiesAlert();
  123. const data = useMemo(() => (isRequestFrame(item) ? item.data : {}), [item]);
  124. useEffect(() => {
  125. if (!isDismissed && 'response' in data) {
  126. dismiss();
  127. }
  128. }, [dismiss, data, isDismissed]);
  129. return (
  130. <SectionItem
  131. title={t('Response Body')}
  132. titleExtra={
  133. <SizeTooltip>
  134. {t('Size:')} {formatBytesBase10(data.response?.size ?? 0)}
  135. </SizeTooltip>
  136. }
  137. >
  138. <Indent>
  139. <Warning warnings={data?.response?._meta?.warnings} />
  140. {'response' in data ? (
  141. <ObjectInspector data={data.response?.body} expandLevel={2} showCopyButton />
  142. ) : (
  143. t('Response body not found.')
  144. )}
  145. </Indent>
  146. </SectionItem>
  147. );
  148. }