content.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import omit from 'lodash/omit';
  5. import {CompactSelect} from 'sentry/components/compactSelect';
  6. import SearchBar from 'sentry/components/events/searchBar';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  9. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  10. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  11. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  12. import Pagination from 'sentry/components/pagination';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {Organization} from 'sentry/types/organization';
  16. import {defined} from 'sentry/utils';
  17. import {trackAnalytics} from 'sentry/utils/analytics';
  18. import {browserHistory} from 'sentry/utils/browserHistory';
  19. import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
  20. import type EventView from 'sentry/utils/discover/eventView';
  21. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  22. import SuspectSpansQuery from 'sentry/utils/performance/suspectSpans/suspectSpansQuery';
  23. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  24. import {decodeScalar} from 'sentry/utils/queryString';
  25. import useLocationQuery from 'sentry/utils/url/useLocationQuery';
  26. import useProjects from 'sentry/utils/useProjects';
  27. import SpanMetricsTable from 'sentry/views/performance/transactionSummary/transactionSpans/spanMetricsTable';
  28. import {useSpanFieldSupportedTags} from 'sentry/views/performance/utils/useSpanFieldSupportedTags';
  29. import type {SetStateAction} from '../types';
  30. import OpsFilter from './opsFilter';
  31. import SuspectSpansTable from './suspectSpansTable';
  32. import type {SpanSort, SpansTotalValues} from './types';
  33. import {
  34. getSuspectSpanSortFromEventView,
  35. getTotalsView,
  36. SPAN_RELATIVE_PERIODS,
  37. SPAN_RETENTION_DAYS,
  38. SPAN_SORT_OPTIONS,
  39. SPAN_SORT_TO_FIELDS,
  40. } from './utils';
  41. const ANALYTICS_VALUES = {
  42. spanOp: (organization: Organization, value: string | undefined) =>
  43. trackAnalytics('performance_views.spans.change_op', {
  44. organization,
  45. operation_name: value,
  46. }),
  47. sort: (organization: Organization, value: string | undefined) =>
  48. trackAnalytics('performance_views.spans.change_sort', {
  49. organization,
  50. sort_column: value,
  51. }),
  52. };
  53. type Props = {
  54. eventView: EventView;
  55. location: Location;
  56. organization: Organization;
  57. projectId: string;
  58. setError: SetStateAction<string | undefined>;
  59. transactionName: string;
  60. };
  61. function SpansContent(props: Props) {
  62. const {location, organization, eventView, projectId, transactionName} = props;
  63. const query = decodeScalar(location.query.query, '');
  64. function handleChange(key: string) {
  65. return function (value: string | undefined) {
  66. ANALYTICS_VALUES[key]?.(organization, value);
  67. const queryParams = normalizeDateTimeParams({
  68. ...(location.query || {}),
  69. [key]: value,
  70. });
  71. // do not propagate pagination when making a new search
  72. const toOmit = ['cursor'];
  73. if (!defined(value)) {
  74. toOmit.push(key);
  75. }
  76. const searchQueryParams = omit(queryParams, toOmit);
  77. browserHistory.push({
  78. ...location,
  79. query: searchQueryParams,
  80. });
  81. };
  82. }
  83. const spanOp = decodeScalar(location.query.spanOp);
  84. const spanGroup = decodeScalar(location.query.spanGroup);
  85. const sort = getSuspectSpanSortFromEventView(eventView);
  86. const spansView = getSpansEventView(eventView, sort.field);
  87. const totalsView = getTotalsView(eventView);
  88. const {projects} = useProjects();
  89. const hasNewSpansUIFlag =
  90. organization.features.includes('performance-spans-new-ui') &&
  91. organization.features.includes('insights-initial-modules');
  92. // TODO: Remove this flag when the feature is GA'd and replace the old content entirely
  93. if (hasNewSpansUIFlag) {
  94. return <SpansContentV2 {...props} />;
  95. }
  96. return (
  97. <Layout.Main fullWidth>
  98. <FilterActions>
  99. <OpsFilter
  100. location={location}
  101. eventView={eventView}
  102. organization={organization}
  103. handleOpChange={handleChange('spanOp')}
  104. transactionName={transactionName}
  105. />
  106. <PageFilterBar condensed>
  107. <EnvironmentPageFilter />
  108. <DatePageFilter
  109. maxPickableDays={SPAN_RETENTION_DAYS}
  110. relativeOptions={SPAN_RELATIVE_PERIODS}
  111. />
  112. </PageFilterBar>
  113. <StyledSearchBar
  114. organization={organization}
  115. projectIds={eventView.project}
  116. query={query}
  117. fields={eventView.fields}
  118. onSearch={handleChange('query')}
  119. />
  120. <CompactSelect
  121. value={sort.field}
  122. options={SPAN_SORT_OPTIONS.map(opt => ({value: opt.field, label: opt.label}))}
  123. onChange={opt => handleChange('sort')(opt.value)}
  124. triggerProps={{prefix: sort.prefix}}
  125. triggerLabel={sort.label}
  126. />
  127. </FilterActions>
  128. <DiscoverQuery
  129. eventView={totalsView}
  130. orgSlug={organization.slug}
  131. location={location}
  132. referrer="api.performance.transaction-spans"
  133. cursor="0:0:1"
  134. noPagination
  135. >
  136. {({tableData}) => {
  137. const totals: SpansTotalValues | null =
  138. (tableData?.data?.[0] as SpansTotalValues | undefined) ?? null;
  139. return (
  140. <SuspectSpansQuery
  141. location={location}
  142. orgSlug={organization.slug}
  143. eventView={spansView}
  144. limit={10}
  145. perSuspect={0}
  146. spanOps={defined(spanOp) ? [spanOp] : []}
  147. spanGroups={defined(spanGroup) ? [spanGroup] : []}
  148. >
  149. {({suspectSpans, isLoading, pageLinks}) => (
  150. <Fragment>
  151. <VisuallyCompleteWithData
  152. id="TransactionSpans-SuspectSpansTable"
  153. hasData={!!suspectSpans?.length}
  154. isLoading={isLoading}
  155. >
  156. <SuspectSpansTable
  157. location={location}
  158. organization={organization}
  159. transactionName={transactionName}
  160. project={projects.find(p => p.id === projectId)}
  161. isLoading={isLoading}
  162. suspectSpans={suspectSpans ?? []}
  163. totals={totals}
  164. sort={sort.field}
  165. />
  166. </VisuallyCompleteWithData>
  167. <Pagination pageLinks={pageLinks ?? null} />
  168. </Fragment>
  169. )}
  170. </SuspectSpansQuery>
  171. );
  172. }}
  173. </DiscoverQuery>
  174. </Layout.Main>
  175. );
  176. }
  177. // TODO: Temporary component while we make the switch to spans only. Will fully replace the old Spans tab when GA'd
  178. function SpansContentV2(props: Props) {
  179. const {location, organization, eventView, projectId, transactionName} = props;
  180. const {projects} = useProjects();
  181. const project = projects.find(p => p.id === projectId);
  182. const query = useLocationQuery({
  183. fields: {
  184. query: decodeScalar,
  185. },
  186. });
  187. function handleChange(key: string) {
  188. return function (value: string | undefined) {
  189. ANALYTICS_VALUES[key]?.(organization, value);
  190. const queryParams = normalizeDateTimeParams({
  191. ...(location.query || {}),
  192. [key]: value,
  193. });
  194. // do not propagate pagination when making a new search
  195. const toOmit = ['cursor'];
  196. if (!defined(value)) {
  197. toOmit.push(key);
  198. }
  199. const searchQueryParams = omit(queryParams, toOmit);
  200. browserHistory.push({
  201. ...location,
  202. query: searchQueryParams,
  203. });
  204. };
  205. }
  206. const supportedTags = useSpanFieldSupportedTags();
  207. return (
  208. <Layout.Main fullWidth>
  209. <FilterActions>
  210. <OpsFilter
  211. location={location}
  212. eventView={eventView}
  213. organization={organization}
  214. handleOpChange={handleChange('spanOp')}
  215. transactionName={transactionName}
  216. />
  217. <PageFilterBar condensed>
  218. <EnvironmentPageFilter />
  219. <DatePageFilter
  220. maxPickableDays={SPAN_RETENTION_DAYS}
  221. relativeOptions={SPAN_RELATIVE_PERIODS}
  222. />
  223. </PageFilterBar>
  224. <StyledSearchBar
  225. organization={organization}
  226. projectIds={eventView.project}
  227. query={query.query}
  228. fields={eventView.fields}
  229. placeholder={t('Search for span attributes')}
  230. supportedTags={supportedTags}
  231. // This dataset is separate from the query itself which is on metrics; it's for obtaining autocomplete recommendations
  232. dataset={DiscoverDatasets.SPANS_INDEXED}
  233. onSearch={handleChange('query')}
  234. />
  235. </FilterActions>
  236. <SpanMetricsTable project={project} transactionName={transactionName} />
  237. </Layout.Main>
  238. );
  239. }
  240. function getSpansEventView(eventView: EventView, sort: SpanSort): EventView {
  241. eventView = eventView.clone();
  242. const fields = SPAN_SORT_TO_FIELDS[sort];
  243. eventView.fields = fields ? fields.map(field => ({field})) : [];
  244. return eventView;
  245. }
  246. const FilterActions = styled('div')`
  247. display: grid;
  248. gap: ${space(2)};
  249. margin-bottom: ${space(2)};
  250. @media (min-width: ${p => p.theme.breakpoints.small}) {
  251. grid-template-columns: repeat(3, min-content);
  252. }
  253. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  254. grid-template-columns: auto auto 1fr auto;
  255. }
  256. `;
  257. const StyledSearchBar = styled(SearchBar)`
  258. @media (min-width: ${p => p.theme.breakpoints.small}) {
  259. order: 1;
  260. grid-column: 1/5;
  261. }
  262. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  263. order: initial;
  264. grid-column: auto;
  265. }
  266. `;
  267. export default SpansContent;