index.tsx 8.1 KB

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