-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat(suspect-spans): Add suspect spans tab page #29453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import {ReactNode} from 'react'; | ||
| import omit from 'lodash/omit'; | ||
|
|
||
| import GenericDiscoverQuery, { | ||
| DiscoverQueryProps, | ||
| GenericChildrenProps, | ||
| } from 'app/utils/discover/genericDiscoverQuery'; | ||
| import withApi from 'app/utils/withApi'; | ||
|
|
||
| import {SuspectSpans} from './types'; | ||
|
|
||
| type SuspectSpansProps = {}; | ||
|
|
||
| type RequestProps = DiscoverQueryProps & SuspectSpansProps; | ||
|
|
||
| type ChildrenProps = Omit<GenericChildrenProps<SuspectSpansProps>, 'tableData'> & { | ||
| suspectSpans: SuspectSpans | null; | ||
| }; | ||
|
|
||
| type Props = RequestProps & { | ||
| children: (props: ChildrenProps) => ReactNode; | ||
| }; | ||
|
|
||
| function SuspectSpansQuery(props: Props) { | ||
| return ( | ||
| <GenericDiscoverQuery<SuspectSpans, SuspectSpansProps> | ||
| route="events-spans-performance" | ||
| limit={4} | ||
| {...omit(props, 'children')} | ||
| > | ||
| {({tableData, ...rest}) => { | ||
| return props.children({suspectSpans: tableData, ...rest}); | ||
| }} | ||
| </GenericDiscoverQuery> | ||
| ); | ||
| } | ||
|
|
||
| export default withApi(SuspectSpansQuery); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| export type ExampleSpan = { | ||
| id: string; | ||
| startTimestamp: number; | ||
| finishTimestamp: number; | ||
| exclusiveTime: number; | ||
| }; | ||
|
|
||
| export type ExampleTransaction = { | ||
| id: string; | ||
| description: string | null; | ||
| startTimestamp: number; | ||
| finishTimestamp: number; | ||
| nonOverlappingExclusiveTime: number; | ||
| spans: ExampleSpan[]; | ||
| }; | ||
|
|
||
| export type SuspectSpan = { | ||
| projectId: number; | ||
| project: string; | ||
| transaction: string; | ||
| op: string; | ||
| group: string; | ||
| frequency: number; | ||
| count: number; | ||
| sumExclusiveTime: number; | ||
| p50ExclusiveTime: number; | ||
| p75ExclusiveTime: number; | ||
| p95ExclusiveTime: number; | ||
| p99ExclusiveTime: number; | ||
| examples: ExampleTransaction[]; | ||
| }; | ||
|
|
||
| export type SuspectSpans = SuspectSpan[]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,122 @@ | ||
| import {Fragment} from 'react'; | ||
| import {browserHistory} from 'react-router'; | ||
| import {Location} from 'history'; | ||
| import omit from 'lodash/omit'; | ||
|
|
||
| import DropdownControl, {DropdownItem} from 'app/components/dropdownControl'; | ||
| import SearchBar from 'app/components/events/searchBar'; | ||
| import * as Layout from 'app/components/layouts/thirds'; | ||
| import LoadingIndicator from 'app/components/loadingIndicator'; | ||
| import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams'; | ||
| import Pagination from 'app/components/pagination'; | ||
| import {Organization} from 'app/types'; | ||
| import EventView from 'app/utils/discover/eventView'; | ||
| import SuspectSpansQuery from 'app/utils/performance/suspectSpans/suspectSpansQuery'; | ||
| import {decodeScalar} from 'app/utils/queryString'; | ||
|
|
||
| import {SetStateAction} from '../types'; | ||
| import {generateTransactionLink} from '../utils'; | ||
|
|
||
| import {Actions} from './styles'; | ||
| import SuspectSpanCard from './suspectSpanCard'; | ||
| import {getSuspectSpanSortFromEventView, SPAN_SORT_OPTIONS} from './utils'; | ||
|
|
||
| type Props = { | ||
| location: Location; | ||
| organization: Organization; | ||
| eventView: EventView; | ||
| setError: SetStateAction<string | undefined>; | ||
| transactionName: string; | ||
| }; | ||
|
|
||
| function SpansContent(_props: Props) { | ||
| return <p>spans</p>; | ||
| function SpansContent(props: Props) { | ||
| const {location, organization, eventView, setError, transactionName} = props; | ||
| const query = decodeScalar(location.query.query, ''); | ||
|
|
||
| function handleChange(key: string) { | ||
| return function (value: string) { | ||
| const queryParams = getParams({ | ||
| ...(location.query || {}), | ||
| [key]: value, | ||
| }); | ||
|
|
||
| // do not propagate pagination when making a new search | ||
| const searchQueryParams = omit(queryParams, 'cursor'); | ||
|
|
||
| browserHistory.push({ | ||
| ...location, | ||
| query: searchQueryParams, | ||
| }); | ||
| }; | ||
| } | ||
|
|
||
| const sort = getSuspectSpanSortFromEventView(eventView); | ||
|
|
||
| return ( | ||
| <Layout.Main fullWidth> | ||
| <Actions> | ||
| <SearchBar | ||
| organization={organization} | ||
| projectIds={eventView.project} | ||
| query={query} | ||
| fields={eventView.fields} | ||
| onSearch={handleChange('query')} | ||
| /> | ||
| <DropdownControl buttonProps={{prefix: sort.prefix}} label={sort.label}> | ||
| {SPAN_SORT_OPTIONS.map(option => ( | ||
| <DropdownItem | ||
| key={option.field} | ||
| eventKey={option.field} | ||
| isActive={option.field === sort.field} | ||
| onSelect={handleChange('sort')} | ||
| > | ||
| {option.label} | ||
| </DropdownItem> | ||
| ))} | ||
| </DropdownControl> | ||
| </Actions> | ||
| <SuspectSpansQuery | ||
| location={location} | ||
| orgSlug={organization.slug} | ||
| eventView={eventView} | ||
| > | ||
| {({suspectSpans, isLoading, error, pageLinks}) => { | ||
| if (error) { | ||
| setError(error); | ||
| return null; | ||
| } | ||
|
|
||
| // make sure to clear the clear the error message | ||
| setError(undefined); | ||
|
|
||
| if (isLoading) { | ||
| return <LoadingIndicator />; | ||
| } | ||
|
|
||
| if (!suspectSpans?.length) { | ||
| // TODO: empty state | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. Was just coming to look for this but I see it's already a TODO, I'd just actually put a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do that in a follow up |
||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <Fragment> | ||
| {suspectSpans.map(suspectSpan => ( | ||
| <SuspectSpanCard | ||
| key={`${suspectSpan.op}-${suspectSpan.group}`} | ||
| location={location} | ||
| organization={organization} | ||
| suspectSpan={suspectSpan} | ||
| generateTransactionLink={generateTransactionLink(transactionName)} | ||
| eventView={eventView} | ||
| /> | ||
| ))} | ||
| <Pagination pageLinks={pageLinks} /> | ||
| </Fragment> | ||
| ); | ||
| }} | ||
| </SuspectSpansQuery> | ||
| </Layout.Main> | ||
| ); | ||
| } | ||
|
|
||
| export default SpansContent; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import {ReactNode} from 'react'; | ||
| import styled from '@emotion/styled'; | ||
|
|
||
| import {SectionHeading as _SectionHeading} from 'app/components/charts/styles'; | ||
| import {Panel} from 'app/components/panels'; | ||
| import {t} from 'app/locale'; | ||
| import overflowEllipsis from 'app/styles/overflowEllipsis'; | ||
| import space from 'app/styles/space'; | ||
|
|
||
| export const Actions = styled('div')` | ||
| display: grid; | ||
| grid-gap: ${space(2)}; | ||
| grid-template-columns: 1fr min-content; | ||
| align-items: center; | ||
| `; | ||
|
|
||
| export const UpperPanel = styled(Panel)` | ||
| padding: ${space(1.5)} ${space(3)}; | ||
| margin-top: ${space(3)}; | ||
| margin-bottom: 0; | ||
| border-bottom: 0; | ||
| border-bottom-left-radius: 0; | ||
| border-bottom-right-radius: 0; | ||
|
|
||
| display: grid; | ||
|
|
||
| grid-template-columns: 1fr; | ||
| grid-gap: ${space(1.5)}; | ||
|
|
||
| @media (min-width: ${p => p.theme.breakpoints[1]}) { | ||
| grid-template-columns: auto repeat(3, max-content); | ||
| grid-gap: 48px; | ||
| } | ||
| `; | ||
|
|
||
| export const LowerPanel = styled('div')` | ||
| > div { | ||
| border-top-left-radius: 0; | ||
| border-top-right-radius: 0; | ||
| } | ||
| `; | ||
|
|
||
| type HeaderItemProps = { | ||
| label: string; | ||
| value: ReactNode; | ||
| align: 'left' | 'right'; | ||
| }; | ||
|
|
||
| export function HeaderItem(props: HeaderItemProps) { | ||
| const {label, value, align} = props; | ||
|
|
||
| return ( | ||
| <HeaderItemContainer align={align}> | ||
| <SectionHeading>{label}</SectionHeading> | ||
| <SectionValue>{value}</SectionValue> | ||
| </HeaderItemContainer> | ||
| ); | ||
| } | ||
|
|
||
| export const HeaderItemContainer = styled('div')<{align: 'left' | 'right'}>` | ||
| ${overflowEllipsis}; | ||
|
|
||
| @media (min-width: ${p => p.theme.breakpoints[1]}) { | ||
| text-align: ${p => p.align}; | ||
| } | ||
| `; | ||
|
|
||
| const SectionHeading = styled(_SectionHeading)` | ||
| margin: 0; | ||
| `; | ||
|
|
||
| const SectionValue = styled('h1')` | ||
| font-size: ${p => p.theme.headerFontSize}; | ||
| font-weight: normal; | ||
| line-height: 1.2; | ||
| color: ${p => p.theme.textColor}; | ||
| margin-bottom: 0; | ||
| `; | ||
|
|
||
| export const SpanLabelContainer = styled('div')` | ||
| ${overflowEllipsis}; | ||
| `; | ||
|
|
||
| const EmptyValueContainer = styled('span')` | ||
| color: ${p => p.theme.gray300}; | ||
| `; | ||
|
|
||
| export const emptyValue = <EmptyValueContainer>{t('n/a')}</EmptyValueContainer>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Side note (doesn't really matter in this PR):
Instead of having to pass this in, I've also made the
pageErrorcontext now along with an alert. You can just put the provider and alert in the topmost level, and use the hook here without needing it in props anymore.