diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx index 6de38f89b62f8b..18f03841e48b53 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/index.tsx @@ -1,17 +1,21 @@ import React from 'react'; import styled from 'react-emotion'; -import SentryTypes from 'app/sentryTypes'; -import SearchBar from 'app/components/searchBar'; +import PropTypes from 'prop-types'; import {t} from 'app/locale'; +import SearchBar from 'app/components/searchBar'; +import SentryTypes from 'app/sentryTypes'; import {Panel} from 'app/components/panels'; import space from 'app/styles/space'; +import EventView from 'app/views/eventsV2/eventView'; import {SentryTransactionEvent} from './types'; import TraceView from './traceView'; type PropType = { + orgId: string; event: SentryTransactionEvent; + eventView: EventView; }; type State = { @@ -21,6 +25,7 @@ type State = { class SpansInterface extends React.Component { static propTypes = { event: SentryTypes.Event.isRequired, + orgId: PropTypes.string.isRequired, }; state: State = { @@ -34,7 +39,7 @@ class SpansInterface extends React.Component { }; render() { - const {event} = this.props; + const {event, orgId, eventView} = this.props; return (
@@ -45,7 +50,12 @@ class SpansInterface extends React.Component { onSearch={this.handleSpanFilter} /> - +
); diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanBar.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanBar.tsx index 08256f2922098e..b4b0f077b13329 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanBar.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanBar.tsx @@ -9,6 +9,7 @@ import space from 'app/styles/space'; import Count from 'app/components/count'; import Tooltip from 'app/components/tooltip'; import InlineSvg from 'app/components/inlineSvg'; +import EventView from 'app/views/eventsV2/eventView'; import { toPercent, @@ -164,6 +165,7 @@ const getDurationDisplay = ({ }; type SpanBarProps = { + orgId: string; trace: Readonly; span: Readonly; spanBarColour: string; @@ -177,6 +179,7 @@ type SpanBarProps = { isRoot?: boolean; toggleSpanTree: () => void; isCurrentSpanFilteredOut: boolean; + eventView: EventView; }; type SpanBarState = { @@ -218,9 +221,11 @@ class SpanBar extends React.Component { return null; } - const {span} = this.props; + const {span, orgId, isRoot, eventView} = this.props; - return ; + return ( + + ); }; getBounds = (): { diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx index d17f70e42bf762..329a22de3e4347 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanDetail.tsx @@ -3,71 +3,230 @@ import styled from 'react-emotion'; import get from 'lodash/get'; import map from 'lodash/map'; +import {t} from 'app/locale'; import DateTime from 'app/components/dateTime'; import Pills from 'app/components/pills'; import Pill from 'app/components/pill'; import space from 'app/styles/space'; +import withApi from 'app/utils/withApi'; +import {Client} from 'app/api'; +import Button from 'app/components/button'; +import { + generateEventSlug, + generateEventDetailsRoute, +} from 'app/views/eventsV2/eventDetails/utils'; +import EventView from 'app/views/eventsV2/eventView'; +import {generateDiscoverResultsRoute} from 'app/views/eventsV2/results'; import {SpanType} from './types'; -type PropTypes = { +type TransactionResult = { + 'project.name': string; + transaction: string; + id: string; +}; + +type Props = { + api: Client; + orgId: string; span: Readonly; + isRoot: boolean; + eventView: EventView; }; -const SpanDetail = (props: PropTypes) => { - const {span} = props; +type State = { + transactionResults?: TransactionResult[]; +}; - const startTimestamp: number = span.start_timestamp; - const endTimestamp: number = span.timestamp; +class SpanDetail extends React.Component { + state: State = { + transactionResults: undefined, + }; - const duration = (endTimestamp - startTimestamp) * 1000; - const durationString = `${duration.toFixed(3)} ms`; + componentDidMount() { + const {span} = this.props; - return ( - { - // prevent toggling the span detail - event.stopPropagation(); - }} - > - - - {span.span_id} - {span.trace_id} - {span.parent_span_id || ''} - {get(span, 'description', '')} - - - - {` (${startTimestamp})`} - - - - - - {` (${endTimestamp})`} - - - {durationString} - {span.op || ''} - - {String(!!span.same_process_as_parent)} - - - {map(get(span, 'data', {}), (value, key) => { - return ( - - {JSON.stringify(value, null, 4) || ''} - - ); - })} - {JSON.stringify(span, null, 4)} - -
-
- ); -}; + this.fetchSpanDescendents(span.span_id) + .then(response => { + if ( + !response.data || + !Array.isArray(response.data) || + response.data.length <= 0 + ) { + return; + } + + this.setState({ + transactionResults: response.data, + }); + }) + .catch(_error => { + // don't do anything + }); + } + + fetchSpanDescendents(spanID: string): Promise { + const {api, orgId, span} = this.props; + + const url = `/organizations/${orgId}/eventsv2/`; + + const query = { + field: ['transaction', 'id', 'trace.span'], + sort: ['-id'], + query: `event.type:transaction trace:${span.trace_id} trace.parent_span:${spanID}`, + }; + + return api.requestPromise(url, { + method: 'GET', + query, + }); + } + + renderTraversalButton(): React.ReactNode { + if (!this.state.transactionResults || this.state.transactionResults.length <= 0) { + return null; + } + + if (this.state.transactionResults.length === 1) { + const {eventView} = this.props; + + const parentTransactionLink = generateEventDetailsRoute({ + eventSlug: generateSlug(this.state.transactionResults[0]), + orgSlug: this.props.orgId, + }); + + const to = { + pathname: parentTransactionLink, + query: eventView.generateQueryStringObject(), + }; + + return ( +
+ +
+ ); + } + + const {span, orgId} = this.props; + + const eventView = EventView.fromSavedQuery({ + id: undefined, + name: t('Transactions'), + fields: ['transaction', 'trace.span', 'timestamp'], + fieldnames: ['transaction', 'trace.span', 'timestamp'], + orderby: '-timestamp', + query: `event.type:transaction trace:${span.trace_id} trace.parent_span:${ + span.span_id + }`, + tags: ['release', 'project.name', 'user.email', 'user.ip', 'environment'], + projects: [], + version: 2, + }); + + const to = { + pathname: generateDiscoverResultsRoute(orgId), + query: eventView.generateQueryStringObject(), + }; + + return ( +
+ +
+ ); + } + + renderTraceButton() { + const {span, orgId} = this.props; + + const eventView = EventView.fromSavedQuery({ + id: undefined, + name: t('Transactions'), + fields: ['transaction', 'trace.span', 'transaction.duration', 'timestamp'], + fieldnames: ['transaction', 'trace.span', 'duration', 'timestamp'], + orderby: '-timestamp', + query: `event.type:transaction trace:${span.trace_id}`, + tags: ['release', 'project.name', 'user.email', 'user.ip', 'environment'], + projects: [], + version: 2, + }); + + const to = { + pathname: generateDiscoverResultsRoute(orgId), + query: eventView.generateQueryStringObject(), + }; + + return ( +
+ +
+ ); + } + + render() { + const {span} = this.props; + + const startTimestamp: number = span.start_timestamp; + const endTimestamp: number = span.timestamp; + + const duration = (endTimestamp - startTimestamp) * 1000; + const durationString = `${duration.toFixed(3)} ms`; + + return ( + { + // prevent toggling the span detail + event.stopPropagation(); + }} + > + + + + {span.span_id} + + + {span.trace_id} + + {span.parent_span_id || ''} + {get(span, 'description', '')} + + + + {` (${startTimestamp})`} + + + + + + {` (${endTimestamp})`} + + + {durationString} + {span.op || ''} + + {String(!!span.same_process_as_parent)} + + + {map(get(span, 'data', {}), (value, key) => { + return ( + + {JSON.stringify(value, null, 4) || ''} + + ); + })} + {JSON.stringify(span, null, 4)} + +
+
+ ); + } +} const SpanDetailContainer = styled('div')` border-bottom: 1px solid ${p => p.theme.gray1}; @@ -75,14 +234,26 @@ const SpanDetailContainer = styled('div')` cursor: auto; `; +const ValueTd = styled('td')` + display: flex !important; + max-width: 100% !important; + align-items: center; +`; + +const PreValue = styled('pre')` + flex: 1; +`; + const Row = ({ title, keep, children, + extra = null, }: { title: string; keep?: boolean; children: JSX.Element | string; + extra?: React.ReactNode; }) => { if (!keep && !children) { return null; @@ -91,11 +262,12 @@ const Row = ({ return ( {title} - -
+      
+        
           {children}
-        
- + + {extra} + ); }; @@ -127,4 +299,11 @@ const Tags = ({span}: {span: SpanType}) => { ); }; -export default SpanDetail; +function generateSlug(result: TransactionResult): string { + return generateEventSlug({ + id: result.id, + 'project.name': result['project.name'], + }); +} + +export default withApi(SpanDetail); diff --git a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanGroup.tsx b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanGroup.tsx index e7df69ad5d91d4..28c13ffad628dc 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/spans/spanGroup.tsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/spans/spanGroup.tsx @@ -1,10 +1,14 @@ import React from 'react'; +import EventView from 'app/views/eventsV2/eventView'; + import {SpanBoundsType, SpanGeneratedBoundsType} from './utils'; import {SpanType, ParsedTraceType} from './types'; import SpanBar from './spanBar'; type PropType = { + orgId: string; + eventView: EventView; span: Readonly; trace: Readonly; generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType; @@ -57,11 +61,15 @@ class SpanGroup extends React.Component { treeDepth, spanNumber, isCurrentSpanFilteredOut, + orgId, + eventView, } = this.props; return ( { childSpans: Readonly; generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType; }): RenderedSpanTree => { + const {orgId, eventView} = this.props; + const spanBarColour: string = pickSpanBarColour(span.op); const spanChildren: Array = get(childSpans, span.span_id, []); @@ -194,6 +199,8 @@ class SpanTree extends React.Component { {infoMessage} ; searchQuery: string | undefined; + eventView: EventView; }; type State = { @@ -68,8 +71,8 @@ class TraceView extends React.PureComponent { static getDerivedStateFromProps(props: Props, state: State): State { return { - parsedTrace: parseTrace(props.event), ...state, + parsedTrace: parseTrace(props.event), }; } @@ -204,6 +207,7 @@ class TraceView extends React.PureComponent { } const parsedTrace = this.state.parsedTrace; + const {orgId, eventView} = this.props; return ( @@ -216,9 +220,11 @@ class TraceView extends React.PureComponent { > {this.renderHeader(dragProps, parsedTrace)} ); diff --git a/src/sentry/static/sentry/app/views/eventsV2/breadcrumb.tsx b/src/sentry/static/sentry/app/views/eventsV2/breadcrumb.tsx index 84428d24473e81..afd45ce319e623 100644 --- a/src/sentry/static/sentry/app/views/eventsV2/breadcrumb.tsx +++ b/src/sentry/static/sentry/app/views/eventsV2/breadcrumb.tsx @@ -10,6 +10,7 @@ import InlineSvg from 'app/components/inlineSvg'; import space from 'app/styles/space'; import EventView from './eventView'; +import {generateDiscoverResultsRoute} from './results'; type Props = { eventView: EventView; @@ -44,7 +45,7 @@ class DiscoverBreadcrumb extends React.Component { if (eventView && eventView.isValid()) { const eventTarget = { - pathname: `/organizations/${organization.slug}/eventsv2/results/`, + pathname: generateDiscoverResultsRoute(organization.slug), query: eventView.generateQueryStringObject(), }; diff --git a/src/sentry/static/sentry/app/views/eventsV2/data.tsx b/src/sentry/static/sentry/app/views/eventsV2/data.tsx index ee1b9ab6331162..d1cc671f5ab87c 100644 --- a/src/sentry/static/sentry/app/views/eventsV2/data.tsx +++ b/src/sentry/static/sentry/app/views/eventsV2/data.tsx @@ -258,7 +258,7 @@ const eventLink = ( ): React.ReactNode => { const eventSlug = generateEventSlug(data); const pathname = generateEventDetailsRoute({ - organization, + orgSlug: organization.slug, eventSlug, }); @@ -362,7 +362,7 @@ export const SPECIAL_FIELDS: SpecialFields = { renderFunc: (data, {location, organization}) => { const eventSlug = generateEventSlug(data); const pathname = generateEventDetailsRoute({ - organization, + orgSlug: organization.slug, eventSlug, }); @@ -384,7 +384,7 @@ export const SPECIAL_FIELDS: SpecialFields = { renderFunc: (data, {location, organization}) => { const eventSlug = generateEventSlug(data); const pathname = generateEventDetailsRoute({ - organization, + orgSlug: organization.slug, eventSlug, }); diff --git a/src/sentry/static/sentry/app/views/eventsV2/eventDetails/eventInterfaces.tsx b/src/sentry/static/sentry/app/views/eventsV2/eventDetails/eventInterfaces.tsx index fafcbf203bf1c8..7db2748b86a0a5 100644 --- a/src/sentry/static/sentry/app/views/eventsV2/eventDetails/eventInterfaces.tsx +++ b/src/sentry/static/sentry/app/views/eventsV2/eventDetails/eventInterfaces.tsx @@ -58,6 +58,7 @@ const ActiveTab = (props: ActiveTabProps) => { projectId={projectId} orgId={organization.slug} event={event} + eventView={eventView} type={entry.type} data={entry.data} isShare={false} diff --git a/src/sentry/static/sentry/app/views/eventsV2/eventDetails/lineGraph.tsx b/src/sentry/static/sentry/app/views/eventsV2/eventDetails/lineGraph.tsx index 26129a2806dfe9..62f7656b7557f1 100644 --- a/src/sentry/static/sentry/app/views/eventsV2/eventDetails/lineGraph.tsx +++ b/src/sentry/static/sentry/app/views/eventsV2/eventDetails/lineGraph.tsx @@ -143,7 +143,7 @@ const handleClick = async function( const eventSlug = generateEventSlug(event); browserHistory.push({ - pathname: generateEventDetailsRoute({eventSlug, organization}), + pathname: generateEventDetailsRoute({eventSlug, orgSlug: organization.slug}), query: eventView.generateQueryStringObject(), }); }; diff --git a/src/sentry/static/sentry/app/views/eventsV2/eventDetails/linkedEvents.tsx b/src/sentry/static/sentry/app/views/eventsV2/eventDetails/linkedEvents.tsx index 4a6259ecec696f..7b2feb9ba039d5 100644 --- a/src/sentry/static/sentry/app/views/eventsV2/eventDetails/linkedEvents.tsx +++ b/src/sentry/static/sentry/app/views/eventsV2/eventDetails/linkedEvents.tsx @@ -81,7 +81,10 @@ class LinkedEvents extends AsyncComponent { linkedEvents.data.map((item: DiscoverResult) => { const eventSlug = generateEventSlug(item); const eventUrl = { - pathname: generateEventDetailsRoute({eventSlug, organization}), + pathname: generateEventDetailsRoute({ + eventSlug, + orgSlug: organization.slug, + }), query: eventView.generateQueryStringObject(), }; const project = projects.find(p => p.slug === item['project.name']); diff --git a/src/sentry/static/sentry/app/views/eventsV2/eventDetails/pagination.tsx b/src/sentry/static/sentry/app/views/eventsV2/eventDetails/pagination.tsx index 5282188c9f4365..3e32c262919e91 100644 --- a/src/sentry/static/sentry/app/views/eventsV2/eventDetails/pagination.tsx +++ b/src/sentry/static/sentry/app/views/eventsV2/eventDetails/pagination.tsx @@ -44,7 +44,7 @@ function buildTargets( const eventSlug = `${event.projectSlug}:${value}`; links[key] = { - pathname: generateEventDetailsRoute({eventSlug, organization}), + pathname: generateEventDetailsRoute({eventSlug, orgSlug: organization.slug}), query: eventView.generateQueryStringObject(), }; } diff --git a/src/sentry/static/sentry/app/views/eventsV2/eventDetails/utils.tsx b/src/sentry/static/sentry/app/views/eventsV2/eventDetails/utils.tsx index 0ad3ba3e08223e..90b5e4a057858c 100644 --- a/src/sentry/static/sentry/app/views/eventsV2/eventDetails/utils.tsx +++ b/src/sentry/static/sentry/app/views/eventsV2/eventDetails/utils.tsx @@ -1,15 +1,13 @@ -import {Organization} from 'app/types'; - import {EventData} from '../data'; export function generateEventDetailsRoute({ eventSlug, - organization, + orgSlug, }: { eventSlug: string; - organization: Organization; + orgSlug: String; }): string { - return `/organizations/${organization.slug}/eventsv2/${eventSlug}/`; + return `/organizations/${orgSlug}/eventsv2/${eventSlug}/`; } export function generateEventSlug(eventData: EventData): string { diff --git a/src/sentry/static/sentry/app/views/eventsV2/landing.tsx b/src/sentry/static/sentry/app/views/eventsV2/landing.tsx index 5d730c37831f3f..f0136e83613404 100644 --- a/src/sentry/static/sentry/app/views/eventsV2/landing.tsx +++ b/src/sentry/static/sentry/app/views/eventsV2/landing.tsx @@ -28,6 +28,7 @@ import EventView from './eventView'; import {DEFAULT_EVENT_VIEW} from './data'; import QueryList from './queryList'; import {getPrebuiltQueries, decodeScalar} from './utils'; +import {generateDiscoverResultsRoute} from './results'; const BANNER_DISMISSED_KEY = 'discover-banner-dismissed'; @@ -219,7 +220,7 @@ class DiscoverLanding extends AsyncComponent { const eventView = EventView.fromNewQueryWithLocation(DEFAULT_EVENT_VIEW, location); const to = { - pathname: `/organizations/${organization.slug}/eventsV2/results/`, + pathname: generateDiscoverResultsRoute(organization.slug), query: { ...eventView.generateQueryStringObject(), }, diff --git a/src/sentry/static/sentry/app/views/eventsV2/queryList.tsx b/src/sentry/static/sentry/app/views/eventsV2/queryList.tsx index 8994641bfd4838..0b2b98bcfc841e 100644 --- a/src/sentry/static/sentry/app/views/eventsV2/queryList.tsx +++ b/src/sentry/static/sentry/app/views/eventsV2/queryList.tsx @@ -22,6 +22,7 @@ import QueryCard from './querycard'; import MiniGraph from './miniGraph'; import {getPrebuiltQueries} from './utils'; import {handleDeleteQuery, handleCreateQuery} from './savedQuery/utils'; +import {generateDiscoverResultsRoute} from './results'; type Props = { api: Client; @@ -113,7 +114,7 @@ class QueryList extends React.Component { moment(eventView.end).format('MMM D, YYYY h:mm A'); const to = { - pathname: `/organizations/${organization.slug}/eventsV2/results/`, + pathname: generateDiscoverResultsRoute(organization.slug), query: { ...location.query, // remove any landing page cursor @@ -168,7 +169,7 @@ class QueryList extends React.Component { ' - ' + moment(eventView.end).format('MMM D, YYYY h:mm A'); const to = { - pathname: `/organizations/${organization.slug}/eventsV2/results/`, + pathname: generateDiscoverResultsRoute(organization.slug), query: { ...location.query, // remove any landing page cursor diff --git a/src/sentry/static/sentry/app/views/eventsV2/results.tsx b/src/sentry/static/sentry/app/views/eventsV2/results.tsx index 99186c9f5745a3..9970d4e4e60410 100644 --- a/src/sentry/static/sentry/app/views/eventsV2/results.tsx +++ b/src/sentry/static/sentry/app/views/eventsV2/results.tsx @@ -219,4 +219,8 @@ const ContentBox = styled(PageContent)` } `; +export function generateDiscoverResultsRoute(orgSlug: string): string { + return `/organizations/${orgSlug}/eventsv2/results/`; +} + export default withOrganization(Results);