utils.tsx 8.0 KB

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