index.tsx 5.5 KB

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