content.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import {Fragment, useCallback, useMemo} 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 {SpanSearchQueryBuilder} from 'sentry/components/performance/spanSearchQueryBuilder';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Organization} from 'sentry/types/organization';
  17. import {defined} from 'sentry/utils';
  18. import {trackAnalytics} from 'sentry/utils/analytics';
  19. import {browserHistory} from 'sentry/utils/browserHistory';
  20. import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
  21. import type EventView from 'sentry/utils/discover/eventView';
  22. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  23. import SuspectSpansQuery from 'sentry/utils/performance/suspectSpans/suspectSpansQuery';
  24. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  25. import {decodeScalar} from 'sentry/utils/queryString';
  26. import useProjects from 'sentry/utils/useProjects';
  27. import SpanMetricsTable from 'sentry/views/performance/transactionSummary/transactionSpans/spanMetricsTable';
  28. import {useSpanMetricsFieldSupportedTags} 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. const handleChange = useCallback(
  65. (key: string) => {
  66. return function (value: string | undefined) {
  67. ANALYTICS_VALUES[key]?.(organization, value);
  68. const queryParams = normalizeDateTimeParams({
  69. ...(location.query || {}),
  70. [key]: value,
  71. });
  72. // do not propagate pagination when making a new search
  73. const toOmit = ['cursor'];
  74. if (!defined(value)) {
  75. toOmit.push(key);
  76. }
  77. const searchQueryParams = omit(queryParams, toOmit);
  78. browserHistory.push({
  79. ...location,
  80. query: searchQueryParams,
  81. });
  82. };
  83. },
  84. [location, organization]
  85. );
  86. const spanOp = decodeScalar(location.query.spanOp);
  87. const spanGroup = decodeScalar(location.query.spanGroup);
  88. const sort = getSuspectSpanSortFromEventView(eventView);
  89. const spansView = getSpansEventView(eventView, sort.field);
  90. const totalsView = getTotalsView(eventView);
  91. const {projects} = useProjects();
  92. const onSearch = useMemo(() => handleChange('query'), [handleChange]);
  93. const projectIds = useMemo(() => eventView.project.slice(), [eventView]);
  94. const hasNewSpansUIFlag =
  95. organization.features.includes('performance-spans-new-ui') &&
  96. organization.features.includes('insights-initial-modules');
  97. // TODO: Remove this flag when the feature is GA'd and replace the old content entirely
  98. if (hasNewSpansUIFlag) {
  99. return <SpansContentV2 {...props} />;
  100. }
  101. return (
  102. <Layout.Main fullWidth>
  103. <FilterActions>
  104. <OpsFilter
  105. location={location}
  106. eventView={eventView}
  107. organization={organization}
  108. handleOpChange={handleChange('spanOp')}
  109. transactionName={transactionName}
  110. />
  111. <PageFilterBar condensed>
  112. <EnvironmentPageFilter />
  113. <DatePageFilter
  114. maxPickableDays={SPAN_RETENTION_DAYS}
  115. relativeOptions={SPAN_RELATIVE_PERIODS}
  116. />
  117. </PageFilterBar>
  118. <StyledSearchBarWrapper>
  119. {organization.features.includes('search-query-builder-performance') ? (
  120. <SpanSearchQueryBuilder
  121. projects={projectIds}
  122. initialQuery={query}
  123. onSearch={onSearch}
  124. searchSource="transaction_spans"
  125. />
  126. ) : (
  127. <SearchBar
  128. organization={organization}
  129. projectIds={eventView.project}
  130. query={query}
  131. fields={eventView.fields}
  132. onSearch={onSearch}
  133. />
  134. )}
  135. <CompactSelect
  136. value={sort.field}
  137. options={SPAN_SORT_OPTIONS.map(opt => ({value: opt.field, label: opt.label}))}
  138. onChange={opt => handleChange('sort')(opt.value)}
  139. triggerProps={{prefix: sort.prefix}}
  140. triggerLabel={sort.label}
  141. />
  142. </StyledSearchBarWrapper>
  143. </FilterActions>
  144. <DiscoverQuery
  145. eventView={totalsView}
  146. orgSlug={organization.slug}
  147. location={location}
  148. referrer="api.performance.transaction-spans"
  149. cursor="0:0:1"
  150. noPagination
  151. >
  152. {({tableData}) => {
  153. const totals: SpansTotalValues | null =
  154. (tableData?.data?.[0] as SpansTotalValues | undefined) ?? null;
  155. return (
  156. <SuspectSpansQuery
  157. location={location}
  158. orgSlug={organization.slug}
  159. eventView={spansView}
  160. limit={10}
  161. perSuspect={0}
  162. spanOps={defined(spanOp) ? [spanOp] : []}
  163. spanGroups={defined(spanGroup) ? [spanGroup] : []}
  164. >
  165. {({suspectSpans, isLoading, pageLinks}) => (
  166. <Fragment>
  167. <VisuallyCompleteWithData
  168. id="TransactionSpans-SuspectSpansTable"
  169. hasData={!!suspectSpans?.length}
  170. isLoading={isLoading}
  171. >
  172. <SuspectSpansTable
  173. location={location}
  174. organization={organization}
  175. transactionName={transactionName}
  176. project={projects.find(p => p.id === projectId)}
  177. isLoading={isLoading}
  178. suspectSpans={suspectSpans ?? []}
  179. totals={totals}
  180. sort={sort.field}
  181. />
  182. </VisuallyCompleteWithData>
  183. <Pagination pageLinks={pageLinks ?? null} />
  184. </Fragment>
  185. )}
  186. </SuspectSpansQuery>
  187. );
  188. }}
  189. </DiscoverQuery>
  190. </Layout.Main>
  191. );
  192. }
  193. // TODO: Temporary component while we make the switch to spans only. Will fully replace the old Spans tab when GA'd
  194. function SpansContentV2(props: Props) {
  195. const {location, organization, eventView, projectId, transactionName} = props;
  196. const supportedTags = useSpanMetricsFieldSupportedTags();
  197. const {projects} = useProjects();
  198. const project = projects.find(p => p.id === projectId);
  199. const spansQuery = decodeScalar(location.query.spansQuery, '');
  200. const handleChange = useCallback(
  201. (key: string) => {
  202. return function (value: string | undefined) {
  203. ANALYTICS_VALUES[key]?.(organization, value);
  204. const queryParams = normalizeDateTimeParams({
  205. ...(location.query || {}),
  206. [key]: value,
  207. });
  208. // do not propagate pagination when making a new search
  209. const toOmit = ['cursor'];
  210. if (!defined(value)) {
  211. toOmit.push(key);
  212. }
  213. const searchQueryParams = omit(queryParams, toOmit);
  214. browserHistory.push({
  215. ...location,
  216. query: searchQueryParams,
  217. });
  218. };
  219. },
  220. [location, organization]
  221. );
  222. const onSearch = useMemo(() => handleChange('spansQuery'), [handleChange]);
  223. const projectIds = useMemo(() => eventView.project.slice(), [eventView]);
  224. return (
  225. <Layout.Main fullWidth>
  226. <FilterActions>
  227. <OpsFilter
  228. location={location}
  229. eventView={eventView}
  230. organization={organization}
  231. handleOpChange={handleChange('spanOp')}
  232. transactionName={transactionName}
  233. />
  234. <PageFilterBar condensed>
  235. <EnvironmentPageFilter />
  236. <DatePageFilter />
  237. </PageFilterBar>
  238. {organization.features.includes('search-query-builder-performance') ? (
  239. <SpanSearchQueryBuilder
  240. projects={projectIds}
  241. initialQuery={spansQuery}
  242. onSearch={onSearch}
  243. searchSource="transaction_spans"
  244. />
  245. ) : (
  246. <SearchBar
  247. organization={organization}
  248. projectIds={eventView.project}
  249. query={spansQuery}
  250. fields={eventView.fields}
  251. placeholder={t('Search for span attributes')}
  252. supportedTags={supportedTags}
  253. // This dataset is separate from the query itself which is on metrics; it's for obtaining autocomplete recommendations
  254. dataset={DiscoverDatasets.SPANS_INDEXED}
  255. onSearch={handleChange('spansQuery')}
  256. />
  257. )}
  258. </FilterActions>
  259. <SpanMetricsTable
  260. project={project}
  261. transactionName={transactionName}
  262. query={spansQuery ?? ''}
  263. />
  264. </Layout.Main>
  265. );
  266. }
  267. function getSpansEventView(eventView: EventView, sort: SpanSort): EventView {
  268. eventView = eventView.clone();
  269. const fields = SPAN_SORT_TO_FIELDS[sort];
  270. eventView.fields = fields ? fields.map(field => ({field})) : [];
  271. return eventView;
  272. }
  273. const FilterActions = styled('div')`
  274. display: grid;
  275. gap: ${space(2)};
  276. margin-bottom: ${space(2)};
  277. @media (min-width: ${p => p.theme.breakpoints.small}) {
  278. grid-template-columns: repeat(3, min-content);
  279. }
  280. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  281. grid-template-columns: auto auto 1fr auto;
  282. }
  283. `;
  284. const StyledSearchBarWrapper = styled('div')`
  285. @media (min-width: ${p => p.theme.breakpoints.small}) {
  286. order: 1;
  287. grid-column: 1/5;
  288. }
  289. @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
  290. order: initial;
  291. grid-column: auto;
  292. }
  293. `;
  294. export default SpansContent;