index.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import {browserHistory} from 'react-router';
  2. import type {Location} from 'history';
  3. import * as Layout from 'sentry/components/layouts/thirds';
  4. import LoadingIndicator from 'sentry/components/loadingIndicator';
  5. import {t} from 'sentry/locale';
  6. import type {Organization, Project} from 'sentry/types';
  7. import {trackAnalytics} from 'sentry/utils/analytics';
  8. import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
  9. import EventView from 'sentry/utils/discover/eventView';
  10. import {
  11. isAggregateField,
  12. SPAN_OP_BREAKDOWN_FIELDS,
  13. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  14. } from 'sentry/utils/discover/fields';
  15. import {WebVital} from 'sentry/utils/fields';
  16. import {removeHistogramQueryStrings} from 'sentry/utils/performance/histogram';
  17. import {decodeScalar} from 'sentry/utils/queryString';
  18. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  19. import withOrganization from 'sentry/utils/withOrganization';
  20. import withProjects from 'sentry/utils/withProjects';
  21. import {
  22. decodeFilterFromLocation,
  23. filterToLocationQuery,
  24. SpanOperationBreakdownFilter,
  25. } from '../filter';
  26. import type {ChildProps} from '../pageLayout';
  27. import PageLayout from '../pageLayout';
  28. import Tab from '../tabs';
  29. import {ZOOM_END, ZOOM_START} from '../transactionOverview/latencyChart/utils';
  30. import EventsContent from './content';
  31. import {
  32. decodeEventsDisplayFilterFromLocation,
  33. EventsDisplayFilterName,
  34. filterEventsDisplayToLocationQuery,
  35. getEventsFilterOptions,
  36. getPercentilesEventView,
  37. mapPercentileValues,
  38. } from './utils';
  39. type PercentileValues = Record<EventsDisplayFilterName, number>;
  40. type Props = {
  41. location: Location;
  42. organization: Organization;
  43. projects: Project[];
  44. };
  45. function TransactionEvents(props: Props) {
  46. const {location, organization, projects} = props;
  47. return (
  48. <PageLayout
  49. location={location}
  50. organization={organization}
  51. projects={projects}
  52. tab={Tab.EVENTS}
  53. getDocumentTitle={getDocumentTitle}
  54. generateEventView={generateEventView}
  55. childComponent={EventsContentWrapper}
  56. />
  57. );
  58. }
  59. function EventsContentWrapper(props: ChildProps) {
  60. const {
  61. location,
  62. organization,
  63. eventView,
  64. transactionName,
  65. setError,
  66. projectId,
  67. projects,
  68. } = props;
  69. const eventsDisplayFilterName = decodeEventsDisplayFilterFromLocation(location);
  70. const spanOperationBreakdownFilter = decodeFilterFromLocation(location);
  71. const webVital = getWebVital(location);
  72. const percentilesView = getPercentilesEventView(eventView);
  73. const getFilteredEventView = (percentiles: PercentileValues) => {
  74. const filter = getEventsFilterOptions(spanOperationBreakdownFilter, percentiles)[
  75. eventsDisplayFilterName
  76. ];
  77. const filteredEventView = eventView?.clone();
  78. if (filteredEventView && filter?.query) {
  79. const query = new MutableSearch(filteredEventView.query);
  80. filter.query.forEach(item => query.setFilterValues(item[0], [item[1]]));
  81. filteredEventView.query = query.formatString();
  82. }
  83. return filteredEventView;
  84. };
  85. const onChangeSpanOperationBreakdownFilter = (
  86. newFilter: SpanOperationBreakdownFilter
  87. ) => {
  88. trackAnalytics('performance_views.transactionEvents.ops_filter_dropdown.selection', {
  89. organization,
  90. action: newFilter as string,
  91. });
  92. // Check to see if the current table sort matches the EventsDisplayFilter.
  93. // If it does, we can re-sort using the new SpanOperationBreakdownFilter
  94. const eventsFilterOptionSort = getEventsFilterOptions(spanOperationBreakdownFilter)[
  95. eventsDisplayFilterName
  96. ].sort;
  97. const currentSort = eventView?.sorts?.[0];
  98. let sortQuery = {};
  99. if (
  100. eventsFilterOptionSort?.kind === currentSort?.kind &&
  101. eventsFilterOptionSort?.field === currentSort?.field
  102. ) {
  103. sortQuery = filterEventsDisplayToLocationQuery(eventsDisplayFilterName, newFilter);
  104. }
  105. const nextQuery: Location['query'] = {
  106. ...removeHistogramQueryStrings(location, [ZOOM_START, ZOOM_END]),
  107. ...filterToLocationQuery(newFilter),
  108. ...sortQuery,
  109. };
  110. if (newFilter === SpanOperationBreakdownFilter.NONE) {
  111. delete nextQuery.breakdown;
  112. }
  113. browserHistory.push({
  114. pathname: location.pathname,
  115. query: nextQuery,
  116. });
  117. };
  118. const onChangeEventsDisplayFilter = (newFilterName: EventsDisplayFilterName) => {
  119. trackAnalytics(
  120. 'performance_views.transactionEvents.display_filter_dropdown.selection',
  121. {
  122. organization,
  123. action: newFilterName as string,
  124. }
  125. );
  126. const nextQuery: Location['query'] = {
  127. ...removeHistogramQueryStrings(location, [ZOOM_START, ZOOM_END]),
  128. ...filterEventsDisplayToLocationQuery(newFilterName, spanOperationBreakdownFilter),
  129. };
  130. if (newFilterName === EventsDisplayFilterName.P100) {
  131. delete nextQuery.showTransaction;
  132. }
  133. browserHistory.push({
  134. pathname: location.pathname,
  135. query: nextQuery,
  136. });
  137. };
  138. return (
  139. <DiscoverQuery
  140. eventView={percentilesView}
  141. orgSlug={organization.slug}
  142. location={location}
  143. referrer="api.performance.transaction-events"
  144. >
  145. {({isLoading, tableData}) => {
  146. if (isLoading) {
  147. return (
  148. <Layout.Main fullWidth>
  149. <LoadingIndicator />
  150. </Layout.Main>
  151. );
  152. }
  153. const percentileData = tableData?.data?.[0];
  154. const percentiles = mapPercentileValues(percentileData);
  155. const filteredEventView = getFilteredEventView(percentiles);
  156. return (
  157. <EventsContent
  158. location={location}
  159. organization={organization}
  160. eventView={filteredEventView}
  161. transactionName={transactionName}
  162. spanOperationBreakdownFilter={spanOperationBreakdownFilter}
  163. onChangeSpanOperationBreakdownFilter={onChangeSpanOperationBreakdownFilter}
  164. eventsDisplayFilterName={eventsDisplayFilterName}
  165. onChangeEventsDisplayFilter={onChangeEventsDisplayFilter}
  166. percentileValues={percentiles}
  167. projectId={projectId}
  168. projects={projects}
  169. webVital={webVital}
  170. setError={setError}
  171. />
  172. );
  173. }}
  174. </DiscoverQuery>
  175. );
  176. }
  177. function getDocumentTitle(transactionName: string): string {
  178. const hasTransactionName =
  179. typeof transactionName === 'string' && String(transactionName).trim().length > 0;
  180. if (hasTransactionName) {
  181. return [String(transactionName).trim(), t('Events')].join(' \u2014 ');
  182. }
  183. return [t('Summary'), t('Events')].join(' \u2014 ');
  184. }
  185. function getWebVital(location: Location): WebVital | undefined {
  186. const webVital = decodeScalar(location.query.webVital, '') as WebVital;
  187. if (Object.values(WebVital).includes(webVital)) {
  188. return webVital;
  189. }
  190. return undefined;
  191. }
  192. function generateEventView({
  193. location,
  194. transactionName,
  195. }: {
  196. location: Location;
  197. organization: Organization;
  198. transactionName: string;
  199. }): EventView {
  200. const query = decodeScalar(location.query.query, '');
  201. const conditions = new MutableSearch(query);
  202. conditions.setFilterValues('event.type', ['transaction']);
  203. conditions.setFilterValues('transaction', [transactionName]);
  204. Object.keys(conditions.filters).forEach(field => {
  205. if (isAggregateField(field)) {
  206. conditions.removeFilter(field);
  207. }
  208. });
  209. // Default fields for relative span view
  210. const fields = [
  211. 'id',
  212. 'user.display',
  213. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  214. 'transaction.duration',
  215. 'trace',
  216. 'timestamp',
  217. ];
  218. const breakdown = decodeFilterFromLocation(location);
  219. if (breakdown !== SpanOperationBreakdownFilter.NONE) {
  220. fields.splice(2, 1, `spans.${breakdown}`);
  221. } else {
  222. fields.push(...SPAN_OP_BREAKDOWN_FIELDS);
  223. }
  224. const webVital = getWebVital(location);
  225. if (webVital) {
  226. fields.splice(3, 0, webVital);
  227. }
  228. return EventView.fromNewQueryWithLocation(
  229. {
  230. id: undefined,
  231. version: 2,
  232. name: transactionName,
  233. fields,
  234. query: conditions.formatString(),
  235. projects: [],
  236. orderby: decodeScalar(location.query.sort, '-timestamp'),
  237. },
  238. location
  239. );
  240. }
  241. export default withProjects(withOrganization(TransactionEvents));