utils.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import {browserHistory} from 'react-router';
  2. import {Location} from 'history';
  3. import omit from 'lodash/omit';
  4. import {t} from 'sentry/locale';
  5. import {Organization, Project} from 'sentry/types';
  6. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  7. import EventView from 'sentry/utils/discover/eventView';
  8. import {
  9. formatAbbreviatedNumber,
  10. formatFloat,
  11. formatPercentage,
  12. getDuration,
  13. } from 'sentry/utils/formatters';
  14. import {HistogramData} from 'sentry/utils/performance/histogram/types';
  15. import {decodeScalar} from 'sentry/utils/queryString';
  16. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  17. import {AxisOption, getTermHelp, PERFORMANCE_TERM} from '../data';
  18. import {Rectangle} from '../transactionSummary/transactionVitals/types';
  19. import {platformToPerformanceType, PROJECT_PERFORMANCE_TYPE} from '../utils';
  20. export const LEFT_AXIS_QUERY_KEY = 'left';
  21. export const RIGHT_AXIS_QUERY_KEY = 'right';
  22. type LandingDisplay = {
  23. label: string;
  24. field: LandingDisplayField;
  25. };
  26. export enum LandingDisplayField {
  27. ALL = 'all',
  28. FRONTEND_PAGELOAD = 'frontend_pageload',
  29. FRONTEND_OTHER = 'frontend_other',
  30. BACKEND = 'backend',
  31. MOBILE = 'mobile',
  32. }
  33. export const LANDING_DISPLAYS = [
  34. {
  35. label: 'All Transactions',
  36. field: LandingDisplayField.ALL,
  37. },
  38. {
  39. label: 'Frontend (Pageload)',
  40. field: LandingDisplayField.FRONTEND_PAGELOAD,
  41. },
  42. {
  43. label: 'Frontend (Other)',
  44. field: LandingDisplayField.FRONTEND_OTHER,
  45. },
  46. {
  47. label: 'Backend',
  48. field: LandingDisplayField.BACKEND,
  49. },
  50. {
  51. label: 'Mobile',
  52. field: LandingDisplayField.MOBILE,
  53. isShown: (organization: Organization) =>
  54. organization.features.includes('performance-mobile-vitals'),
  55. },
  56. ];
  57. export const LANDING_V3_DISPLAYS = [
  58. {
  59. label: 'All Transactions',
  60. field: LandingDisplayField.ALL,
  61. },
  62. {
  63. label: 'Web Vitals',
  64. field: LandingDisplayField.FRONTEND_PAGELOAD,
  65. },
  66. {
  67. label: 'Frontend',
  68. field: LandingDisplayField.FRONTEND_OTHER,
  69. },
  70. {
  71. label: 'Backend',
  72. field: LandingDisplayField.BACKEND,
  73. },
  74. {
  75. label: 'Mobile',
  76. field: LandingDisplayField.MOBILE,
  77. isShown: (organization: Organization) =>
  78. organization.features.includes('performance-mobile-vitals'),
  79. },
  80. ];
  81. export function excludeTransaction(
  82. transaction: string | React.ReactText,
  83. props: {eventView: EventView; location: Location}
  84. ) {
  85. const {eventView, location} = props;
  86. const searchConditions = new MutableSearch(eventView.query);
  87. searchConditions.addFilterValues('!transaction', [`${transaction}`]);
  88. browserHistory.push({
  89. pathname: location.pathname,
  90. query: {
  91. ...location.query,
  92. cursor: undefined,
  93. query: searchConditions.formatString(),
  94. },
  95. });
  96. }
  97. export function getLandingDisplayFromParam(location: Location) {
  98. const landingField = decodeScalar(location?.query?.landingDisplay);
  99. const display = LANDING_DISPLAYS.find(({field}) => field === landingField);
  100. return display;
  101. }
  102. export function getDefaultDisplayForPlatform(projects: Project[], eventView?: EventView) {
  103. const defaultDisplayField = getDefaultDisplayFieldForPlatform(projects, eventView);
  104. const defaultDisplay = LANDING_DISPLAYS.find(
  105. ({field}) => field === defaultDisplayField
  106. );
  107. return defaultDisplay || LANDING_DISPLAYS[0];
  108. }
  109. export function getCurrentLandingDisplay(
  110. location: Location,
  111. projects: Project[],
  112. eventView?: EventView
  113. ): LandingDisplay {
  114. const display = getLandingDisplayFromParam(location);
  115. if (display) {
  116. return display;
  117. }
  118. return getDefaultDisplayForPlatform(projects, eventView);
  119. }
  120. export function handleLandingDisplayChange(
  121. field: LandingDisplayField,
  122. location: Location,
  123. projects: Project[],
  124. organization: Organization,
  125. eventView?: EventView
  126. ) {
  127. // Transaction op can affect the display and show no results if it is explicitly set.
  128. const query = decodeScalar(location.query.query, '');
  129. const searchConditions = new MutableSearch(query);
  130. searchConditions.removeFilter('transaction.op');
  131. const queryWithConditions = {
  132. ...omit(location.query, ['landingDisplay', 'sort']),
  133. query: searchConditions.formatString(),
  134. };
  135. delete queryWithConditions[LEFT_AXIS_QUERY_KEY];
  136. delete queryWithConditions[RIGHT_AXIS_QUERY_KEY];
  137. const defaultDisplay = getDefaultDisplayFieldForPlatform(projects, eventView);
  138. const currentDisplay = getCurrentLandingDisplay(location, projects, eventView).field;
  139. const newQuery: {query: string; landingDisplay?: LandingDisplayField} =
  140. defaultDisplay === field
  141. ? {...queryWithConditions}
  142. : {...queryWithConditions, landingDisplay: field};
  143. trackAdvancedAnalyticsEvent('performance_views.landingv3.display_change', {
  144. organization,
  145. change_to_display: field,
  146. default_display: defaultDisplay,
  147. current_display: currentDisplay,
  148. is_default: defaultDisplay === currentDisplay,
  149. });
  150. browserHistory.push({
  151. pathname: location.pathname,
  152. query: newQuery,
  153. });
  154. }
  155. export function getChartWidth(chartData: HistogramData, refPixelRect: Rectangle | null) {
  156. const distance = refPixelRect ? refPixelRect.point2.x - refPixelRect.point1.x : 0;
  157. const chartWidth = chartData.length * distance;
  158. return {
  159. chartWidth,
  160. };
  161. }
  162. export function getDefaultDisplayFieldForPlatform(
  163. projects: Project[],
  164. eventView?: EventView
  165. ) {
  166. if (!eventView) {
  167. return LandingDisplayField.ALL;
  168. }
  169. const projectIds = eventView.project;
  170. const performanceTypeToDisplay = {
  171. [PROJECT_PERFORMANCE_TYPE.ANY]: LandingDisplayField.ALL,
  172. [PROJECT_PERFORMANCE_TYPE.FRONTEND]: LandingDisplayField.FRONTEND_PAGELOAD,
  173. [PROJECT_PERFORMANCE_TYPE.BACKEND]: LandingDisplayField.BACKEND,
  174. [PROJECT_PERFORMANCE_TYPE.MOBILE]: LandingDisplayField.MOBILE,
  175. };
  176. const performanceType = platformToPerformanceType(projects, projectIds);
  177. const landingField =
  178. performanceTypeToDisplay[performanceType] ?? LandingDisplayField.ALL;
  179. return landingField;
  180. }
  181. type VitalCardDetail = {
  182. title: string;
  183. tooltip: string;
  184. formatter: (value: number) => string | number;
  185. };
  186. export const vitalCardDetails = (
  187. organization: Organization
  188. ): {[key: string]: VitalCardDetail | undefined} => {
  189. return {
  190. 'p75(transaction.duration)': {
  191. title: t('Duration (p75)'),
  192. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P75),
  193. formatter: value => getDuration(value / 1000, value >= 1000 ? 3 : 0, true),
  194. },
  195. 'tpm()': {
  196. title: t('Throughput'),
  197. tooltip: getTermHelp(organization, PERFORMANCE_TERM.THROUGHPUT),
  198. formatter: formatAbbreviatedNumber,
  199. },
  200. 'failure_rate()': {
  201. title: t('Failure Rate'),
  202. tooltip: getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE),
  203. formatter: value => formatPercentage(value, 2),
  204. },
  205. 'apdex()': {
  206. title: t('Apdex'),
  207. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APDEX_NEW),
  208. formatter: value => formatFloat(value, 4),
  209. },
  210. 'p75(measurements.frames_slow_rate)': {
  211. title: t('Slow Frames (p75)'),
  212. tooltip: getTermHelp(organization, PERFORMANCE_TERM.SLOW_FRAMES),
  213. formatter: value => formatPercentage(value, 2),
  214. },
  215. 'p75(measurements.frames_frozen_rate)': {
  216. title: t('Frozen Frames (p75)'),
  217. tooltip: getTermHelp(organization, PERFORMANCE_TERM.FROZEN_FRAMES),
  218. formatter: value => formatPercentage(value, 2),
  219. },
  220. 'p75(measurements.app_start_cold)': {
  221. title: t('Cold Start (p75)'),
  222. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_COLD),
  223. formatter: value => getDuration(value / 1000, value >= 1000 ? 3 : 0, true),
  224. },
  225. 'p75(measurements.app_start_warm)': {
  226. title: t('Warm Start (p75)'),
  227. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_WARM),
  228. formatter: value => getDuration(value / 1000, value >= 1000 ? 3 : 0, true),
  229. },
  230. 'p75(measurements.stall_percentage)': {
  231. title: t('Stall Percentage (p75)'),
  232. tooltip: getTermHelp(organization, PERFORMANCE_TERM.STALL_PERCENTAGE),
  233. formatter: value => formatPercentage(value, 2),
  234. },
  235. };
  236. };
  237. export function getDisplayAxes(options: AxisOption[], location: Location) {
  238. const leftDefault = options.find(opt => opt.isLeftDefault) || options[0];
  239. const rightDefault = options.find(opt => opt.isRightDefault) || options[1];
  240. const leftAxis =
  241. options.find(opt => opt.value === location.query[LEFT_AXIS_QUERY_KEY]) || leftDefault;
  242. const rightAxis =
  243. options.find(opt => opt.value === location.query[RIGHT_AXIS_QUERY_KEY]) ||
  244. rightDefault;
  245. return {
  246. leftAxis,
  247. rightAxis,
  248. };
  249. }
  250. export function checkIsReactNative(eventView) {
  251. // only react native should contain the stall percentage column
  252. return Boolean(
  253. eventView.getFields().find(field => field.includes('measurements.stall_percentage'))
  254. );
  255. }