index.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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 trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  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. trackAdvancedAnalyticsEvent(
  88. 'performance_views.transactionEvents.ops_filter_dropdown.selection',
  89. {
  90. organization,
  91. action: newFilter as string,
  92. }
  93. );
  94. // Check to see if the current table sort matches the EventsDisplayFilter.
  95. // If it does, we can re-sort using the new SpanOperationBreakdownFilter
  96. const eventsFilterOptionSort = getEventsFilterOptions(spanOperationBreakdownFilter)[
  97. eventsDisplayFilterName
  98. ].sort;
  99. const currentSort = eventView?.sorts?.[0];
  100. let sortQuery = {};
  101. if (
  102. eventsFilterOptionSort?.kind === currentSort?.kind &&
  103. eventsFilterOptionSort?.field === currentSort?.field
  104. ) {
  105. sortQuery = filterEventsDisplayToLocationQuery(eventsDisplayFilterName, newFilter);
  106. }
  107. const nextQuery: Location['query'] = {
  108. ...removeHistogramQueryStrings(location, [ZOOM_START, ZOOM_END]),
  109. ...filterToLocationQuery(newFilter),
  110. ...sortQuery,
  111. };
  112. if (newFilter === SpanOperationBreakdownFilter.None) {
  113. delete nextQuery.breakdown;
  114. }
  115. browserHistory.push({
  116. pathname: location.pathname,
  117. query: nextQuery,
  118. });
  119. };
  120. const onChangeEventsDisplayFilter = (newFilterName: EventsDisplayFilterName) => {
  121. trackAdvancedAnalyticsEvent(
  122. 'performance_views.transactionEvents.display_filter_dropdown.selection',
  123. {
  124. organization,
  125. action: newFilterName as string,
  126. }
  127. );
  128. const nextQuery: Location['query'] = {
  129. ...removeHistogramQueryStrings(location, [ZOOM_START, ZOOM_END]),
  130. ...filterEventsDisplayToLocationQuery(newFilterName, spanOperationBreakdownFilter),
  131. };
  132. if (newFilterName === EventsDisplayFilterName.p100) {
  133. delete nextQuery.showTransaction;
  134. }
  135. browserHistory.push({
  136. pathname: location.pathname,
  137. query: nextQuery,
  138. });
  139. };
  140. return (
  141. <DiscoverQuery
  142. eventView={percentilesView}
  143. orgSlug={organization.slug}
  144. location={location}
  145. referrer="api.performance.transaction-events"
  146. >
  147. {({isLoading, tableData}) => {
  148. if (isLoading) {
  149. return (
  150. <Layout.Main fullWidth>
  151. <LoadingIndicator />
  152. </Layout.Main>
  153. );
  154. }
  155. const percentileData = tableData?.data?.[0];
  156. const percentiles = mapPercentileValues(percentileData);
  157. const filteredEventView = getFilteredEventView(percentiles);
  158. return (
  159. <EventsContent
  160. location={location}
  161. organization={organization}
  162. eventView={filteredEventView}
  163. transactionName={transactionName}
  164. spanOperationBreakdownFilter={spanOperationBreakdownFilter}
  165. onChangeSpanOperationBreakdownFilter={onChangeSpanOperationBreakdownFilter}
  166. eventsDisplayFilterName={eventsDisplayFilterName}
  167. onChangeEventsDisplayFilter={onChangeEventsDisplayFilter}
  168. percentileValues={percentiles}
  169. projectId={projectId}
  170. projects={projects}
  171. webVital={webVital}
  172. setError={setError}
  173. />
  174. );
  175. }}
  176. </DiscoverQuery>
  177. );
  178. }
  179. function getDocumentTitle(transactionName: string): string {
  180. const hasTransactionName =
  181. typeof transactionName === 'string' && String(transactionName).trim().length > 0;
  182. if (hasTransactionName) {
  183. return [String(transactionName).trim(), t('Events')].join(' \u2014 ');
  184. }
  185. return [t('Summary'), t('Events')].join(' \u2014 ');
  186. }
  187. function getWebVital(location: Location): WebVital | undefined {
  188. const webVital = decodeScalar(location.query.webVital, '') as WebVital;
  189. if (Object.values(WebVital).includes(webVital)) {
  190. return webVital;
  191. }
  192. return undefined;
  193. }
  194. function generateEventView({
  195. location,
  196. organization,
  197. transactionName,
  198. }: {
  199. location: Location;
  200. organization: Organization;
  201. transactionName: string;
  202. }): EventView {
  203. const query = decodeScalar(location.query.query, '');
  204. const conditions = new MutableSearch(query);
  205. conditions.setFilterValues('event.type', ['transaction']);
  206. conditions.setFilterValues('transaction', [transactionName]);
  207. Object.keys(conditions.filters).forEach(field => {
  208. if (isAggregateField(field)) {
  209. conditions.removeFilter(field);
  210. }
  211. });
  212. // Default fields for relative span view
  213. const fields = [
  214. 'id',
  215. 'user.display',
  216. SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
  217. 'transaction.duration',
  218. 'trace',
  219. 'timestamp',
  220. ];
  221. if (organization.features.includes('session-replay-ui')) {
  222. fields.push('replayId');
  223. }
  224. const breakdown = decodeFilterFromLocation(location);
  225. if (breakdown !== SpanOperationBreakdownFilter.None) {
  226. fields.splice(2, 1, `spans.${breakdown}`);
  227. } else {
  228. fields.push(...SPAN_OP_BREAKDOWN_FIELDS);
  229. }
  230. const webVital = getWebVital(location);
  231. if (webVital) {
  232. fields.splice(3, 0, webVital);
  233. }
  234. return EventView.fromNewQueryWithLocation(
  235. {
  236. id: undefined,
  237. version: 2,
  238. name: transactionName,
  239. fields,
  240. query: conditions.formatString(),
  241. projects: [],
  242. orderby: decodeScalar(location.query.sort, '-timestamp'),
  243. },
  244. location
  245. );
  246. }
  247. export default withProjects(withOrganization(TransactionEvents));