content.tsx 9.6 KB

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