index.tsx 8.0 KB

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