123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- import {Fragment, useState} from 'react';
- import styled from '@emotion/styled';
- import ClippedBox from 'sentry/components/clippedBox';
- import ErrorBoundary from 'sentry/components/errorBoundary';
- import {EventDataSection} from 'sentry/components/events/eventDataSection';
- import {GraphQlRequestBody} from 'sentry/components/events/interfaces/request/graphQlRequestBody';
- import {getCurlCommand, getFullUrl} from 'sentry/components/events/interfaces/utils';
- import ExternalLink from 'sentry/components/links/externalLink';
- import {SegmentedControl} from 'sentry/components/segmentedControl';
- import Truncate from 'sentry/components/truncate';
- import {IconOpen} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {EntryRequest, EntryType, Event} from 'sentry/types/event';
- import {defined, isUrl} from 'sentry/utils';
- import {RichHttpContentClippedBoxBodySection} from './richHttpContentClippedBoxBodySection';
- import {RichHttpContentClippedBoxKeyValueList} from './richHttpContentClippedBoxKeyValueList';
- interface RequestProps {
- data: EntryRequest['data'];
- event: Event;
- }
- interface RequestBodyProps extends RequestProps {
- meta: any;
- }
- type View = 'formatted' | 'curl';
- function RequestBodySection({data, event, meta}: RequestBodyProps) {
- if (!defined(data.data)) {
- return null;
- }
- if (data.apiTarget === 'graphql' && typeof data.data.query === 'string') {
- return <GraphQlRequestBody data={data.data} {...{event, meta}} />;
- }
- return (
- <RichHttpContentClippedBoxBodySection
- data={data.data}
- inferredContentType={data.inferredContentType}
- meta={meta?.data}
- />
- );
- }
- export function Request({data, event}: RequestProps) {
- const entryIndex = event.entries.findIndex(entry => entry.type === EntryType.REQUEST);
- const meta = event._meta?.entries?.[entryIndex]?.data;
- const [view, setView] = useState<View>('formatted');
- const isPartial =
- // We assume we only have a partial interface is we're missing
- // an HTTP method. This means we don't have enough information
- // to reliably construct a full HTTP request.
- !data.method || !data.url;
- let fullUrl = getFullUrl(data);
- if (!isUrl(fullUrl)) {
- // Check if the url passed in is a safe url to avoid XSS
- fullUrl = undefined;
- }
- let parsedUrl: HTMLAnchorElement | null = null;
- if (fullUrl) {
- // use html tag to parse url, lol
- parsedUrl = document.createElement('a');
- parsedUrl.href = fullUrl;
- }
- let actions: React.ReactNode = null;
- if (!isPartial && fullUrl) {
- actions = (
- <SegmentedControl aria-label={t('View')} size="xs" value={view} onChange={setView}>
- <SegmentedControl.Item key="formatted">
- {/* Translators: this means "formatted" rendering (fancy tables) */}
- {t('Formatted')}
- </SegmentedControl.Item>
- <SegmentedControl.Item key="curl" textValue="curl">
- <Monospace>curl</Monospace>
- </SegmentedControl.Item>
- </SegmentedControl>
- );
- }
- const title = (
- <Fragment>
- <ExternalLink href={fullUrl} title={fullUrl}>
- <Path>
- <strong>{data.method || 'GET'}</strong>
- <Truncate value={parsedUrl ? parsedUrl.pathname : ''} maxLength={36} leftTrim />
- </Path>
- {fullUrl && <StyledIconOpen size="xs" />}
- </ExternalLink>
- <small>{parsedUrl ? parsedUrl.hostname : ''}</small>
- </Fragment>
- );
- return (
- <EventDataSection
- type={EntryType.REQUEST}
- title={title}
- actions={actions}
- className="request"
- >
- {view === 'curl' ? (
- <pre>{getCurlCommand(data)}</pre>
- ) : (
- <Fragment>
- {defined(data.query) && (
- <RichHttpContentClippedBoxKeyValueList
- title={t('Query String')}
- data={data.query}
- meta={meta?.query}
- isContextData
- />
- )}
- {defined(data.fragment) && (
- <ClippedBox title={t('Fragment')}>
- <ErrorBoundary mini>
- <pre>{data.fragment}</pre>
- </ErrorBoundary>
- </ClippedBox>
- )}
- <RequestBodySection {...{data, event, meta}} />
- {defined(data.cookies) && Object.keys(data.cookies).length > 0 && (
- <RichHttpContentClippedBoxKeyValueList
- defaultCollapsed
- title={t('Cookies')}
- data={data.cookies}
- meta={meta?.cookies}
- />
- )}
- {defined(data.headers) && (
- <RichHttpContentClippedBoxKeyValueList
- title={t('Headers')}
- data={data.headers}
- meta={meta?.headers}
- />
- )}
- {defined(data.env) && (
- <RichHttpContentClippedBoxKeyValueList
- defaultCollapsed
- title={t('Environment')}
- data={data.env}
- meta={meta?.env}
- />
- )}
- </Fragment>
- )}
- </EventDataSection>
- );
- }
- const Monospace = styled('span')`
- font-family: ${p => p.theme.text.familyMono};
- `;
- const Path = styled('span')`
- color: ${p => p.theme.textColor};
- text-transform: none;
- font-weight: normal;
- & strong {
- margin-right: ${space(0.5)};
- }
- `;
- // Nudge the icon down so it is centered. the `external-icon` class
- // doesn't quite get it in place.
- const StyledIconOpen = styled(IconOpen)`
- transition: 0.1s linear color;
- margin: 0 ${space(0.5)};
- color: ${p => p.theme.subText};
- position: relative;
- top: 1px;
- &:hover {
- color: ${p => p.theme.textColor};
- }
- `;
|