index.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import ClippedBox from 'sentry/components/clippedBox';
  4. import ErrorBoundary from 'sentry/components/errorBoundary';
  5. import {EventDataSection} from 'sentry/components/events/eventDataSection';
  6. import {getCurlCommand, getFullUrl} from 'sentry/components/events/interfaces/utils';
  7. import ExternalLink from 'sentry/components/links/externalLink';
  8. import {SegmentedControl} from 'sentry/components/segmentedControl';
  9. import Truncate from 'sentry/components/truncate';
  10. import {IconOpen} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import space from 'sentry/styles/space';
  13. import {EntryRequest, EntryType, Event} from 'sentry/types/event';
  14. import {defined, isUrl} from 'sentry/utils';
  15. import {RichHttpContentClippedBoxBodySection} from './richHttpContentClippedBoxBodySection';
  16. import {RichHttpContentClippedBoxKeyValueList} from './richHttpContentClippedBoxKeyValueList';
  17. type Props = {
  18. data: EntryRequest['data'];
  19. event: Event;
  20. };
  21. type View = 'formatted' | 'curl';
  22. export function Request({data, event}: Props) {
  23. const entryIndex = event.entries.findIndex(entry => entry.type === EntryType.REQUEST);
  24. const meta = event._meta?.entries?.[entryIndex]?.data;
  25. const [view, setView] = useState<View>('formatted');
  26. const isPartial =
  27. // We assume we only have a partial interface is we're missing
  28. // an HTTP method. This means we don't have enough information
  29. // to reliably construct a full HTTP request.
  30. !data.method || !data.url;
  31. let fullUrl = getFullUrl(data);
  32. if (!isUrl(fullUrl)) {
  33. // Check if the url passed in is a safe url to avoid XSS
  34. fullUrl = undefined;
  35. }
  36. let parsedUrl: HTMLAnchorElement | null = null;
  37. if (fullUrl) {
  38. // use html tag to parse url, lol
  39. parsedUrl = document.createElement('a');
  40. parsedUrl.href = fullUrl;
  41. }
  42. let actions: React.ReactNode = null;
  43. if (!isPartial && fullUrl) {
  44. actions = (
  45. <SegmentedControl aria-label={t('View')} size="xs" value={view} onChange={setView}>
  46. <SegmentedControl.Item key="formatted">
  47. {/* Translators: this means "formatted" rendering (fancy tables) */}
  48. {t('Formatted')}
  49. </SegmentedControl.Item>
  50. <SegmentedControl.Item key="curl" textValue="curl">
  51. <Monospace>curl</Monospace>
  52. </SegmentedControl.Item>
  53. </SegmentedControl>
  54. );
  55. }
  56. const title = (
  57. <Fragment>
  58. <ExternalLink href={fullUrl} title={fullUrl}>
  59. <Path>
  60. <strong>{data.method || 'GET'}</strong>
  61. <Truncate value={parsedUrl ? parsedUrl.pathname : ''} maxLength={36} leftTrim />
  62. </Path>
  63. {fullUrl && <StyledIconOpen size="xs" />}
  64. </ExternalLink>
  65. <small>{parsedUrl ? parsedUrl.hostname : ''}</small>
  66. </Fragment>
  67. );
  68. return (
  69. <EventDataSection
  70. type={EntryType.REQUEST}
  71. title={title}
  72. actions={actions}
  73. className="request"
  74. >
  75. {view === 'curl' ? (
  76. <pre>{getCurlCommand(data)}</pre>
  77. ) : (
  78. <Fragment>
  79. {defined(data.query) && (
  80. <RichHttpContentClippedBoxKeyValueList
  81. title={t('Query String')}
  82. data={data.query}
  83. meta={meta?.query}
  84. isContextData
  85. />
  86. )}
  87. {defined(data.fragment) && (
  88. <ClippedBox title={t('Fragment')}>
  89. <ErrorBoundary mini>
  90. <pre>{data.fragment}</pre>
  91. </ErrorBoundary>
  92. </ClippedBox>
  93. )}
  94. {defined(data.data) && (
  95. <RichHttpContentClippedBoxBodySection
  96. data={data.data}
  97. inferredContentType={data.inferredContentType}
  98. meta={meta?.data}
  99. />
  100. )}
  101. {defined(data.cookies) && Object.keys(data.cookies).length > 0 && (
  102. <RichHttpContentClippedBoxKeyValueList
  103. defaultCollapsed
  104. title={t('Cookies')}
  105. data={data.cookies}
  106. meta={meta?.cookies}
  107. />
  108. )}
  109. {defined(data.headers) && (
  110. <RichHttpContentClippedBoxKeyValueList
  111. title={t('Headers')}
  112. data={data.headers}
  113. meta={meta?.headers}
  114. />
  115. )}
  116. {defined(data.env) && (
  117. <RichHttpContentClippedBoxKeyValueList
  118. defaultCollapsed
  119. title={t('Environment')}
  120. data={data.env}
  121. meta={meta?.env}
  122. />
  123. )}
  124. </Fragment>
  125. )}
  126. </EventDataSection>
  127. );
  128. }
  129. const Monospace = styled('span')`
  130. font-family: ${p => p.theme.text.familyMono};
  131. `;
  132. const Path = styled('span')`
  133. color: ${p => p.theme.textColor};
  134. text-transform: none;
  135. font-weight: normal;
  136. & strong {
  137. margin-right: ${space(0.5)};
  138. }
  139. `;
  140. // Nudge the icon down so it is centered. the `external-icon` class
  141. // doesn't quite get it in place.
  142. const StyledIconOpen = styled(IconOpen)`
  143. transition: 0.1s linear color;
  144. margin: 0 ${space(0.5)};
  145. color: ${p => p.theme.subText};
  146. position: relative;
  147. top: 1px;
  148. &:hover {
  149. color: ${p => p.theme.textColor};
  150. }
  151. `;