utils.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import type {Theme} from '@emotion/react';
  2. import type {Location, Query} from 'history';
  3. import MarkLine from 'sentry/components/charts/components/markLine';
  4. import type {LineChartProps} from 'sentry/components/charts/lineChart';
  5. import {getSeriesSelection} from 'sentry/components/charts/utils';
  6. import {IconHappy, IconMeh, IconSad} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import type {Series} from 'sentry/types/echarts';
  9. import {axisLabelFormatter, tooltipFormatter} from 'sentry/utils/discover/charts';
  10. import {aggregateOutputType, getAggregateAlias} from 'sentry/utils/discover/fields';
  11. import {WebVital} from 'sentry/utils/fields';
  12. import {Browser} from 'sentry/utils/performance/vitals/constants';
  13. import {decodeScalar} from 'sentry/utils/queryString';
  14. import type {Color} from 'sentry/utils/theme';
  15. import type {AlertType} from 'sentry/views/alerts/wizard/options';
  16. export function generateVitalDetailRoute({orgSlug}: {orgSlug: string}): string {
  17. return `/organizations/${orgSlug}/performance/vitaldetail/`;
  18. }
  19. export const webVitalPoor = {
  20. [WebVital.FP]: 3000,
  21. [WebVital.FCP]: 3000,
  22. [WebVital.LCP]: 4000,
  23. [WebVital.FID]: 300,
  24. [WebVital.CLS]: 0.25,
  25. };
  26. export const webVitalMeh = {
  27. [WebVital.FP]: 1000,
  28. [WebVital.FCP]: 1000,
  29. [WebVital.LCP]: 2500,
  30. [WebVital.FID]: 100,
  31. [WebVital.CLS]: 0.1,
  32. };
  33. export enum VitalState {
  34. POOR = 'Poor',
  35. MEH = 'Meh',
  36. GOOD = 'Good',
  37. }
  38. export const vitalStateColors: Record<VitalState, Color> = {
  39. [VitalState.POOR]: 'red300',
  40. [VitalState.MEH]: 'yellow300',
  41. [VitalState.GOOD]: 'green300',
  42. };
  43. export const vitalStateIcons: Record<VitalState, React.ReactNode> = {
  44. [VitalState.POOR]: <IconSad color={vitalStateColors[VitalState.POOR]} />,
  45. [VitalState.MEH]: <IconMeh color={vitalStateColors[VitalState.MEH]} />,
  46. [VitalState.GOOD]: <IconHappy color={vitalStateColors[VitalState.GOOD]} />,
  47. };
  48. export function vitalDetailRouteWithQuery({
  49. orgSlug,
  50. vitalName,
  51. projectID,
  52. query,
  53. }: {
  54. orgSlug: string;
  55. query: Query;
  56. vitalName: string;
  57. projectID?: string | string[];
  58. }) {
  59. const pathname = generateVitalDetailRoute({
  60. orgSlug,
  61. });
  62. return {
  63. pathname,
  64. query: {
  65. vitalName,
  66. project: projectID,
  67. environment: query.environment,
  68. statsPeriod: query.statsPeriod,
  69. start: query.start,
  70. end: query.end,
  71. query: query.query,
  72. },
  73. };
  74. }
  75. export function vitalNameFromLocation(location: Location): WebVital {
  76. const _vitalName = decodeScalar(location.query.vitalName);
  77. const vitalName = Object.values(WebVital).find(v => v === _vitalName);
  78. if (vitalName) {
  79. return vitalName;
  80. }
  81. return WebVital.LCP;
  82. }
  83. export function getVitalChartTitle(webVital: WebVital): string {
  84. if (webVital === WebVital.CLS) {
  85. return t('CLS p75');
  86. }
  87. return t('Duration p75');
  88. }
  89. export function getVitalDetailTablePoorStatusFunction(vitalName: WebVital): string {
  90. const vitalThreshold = webVitalPoor[vitalName];
  91. const statusFunction = `compare_numeric_aggregate(${getAggregateAlias(
  92. `p75(${vitalName})`
  93. )},greater,${vitalThreshold})`;
  94. return statusFunction;
  95. }
  96. export function getVitalDetailTableMehStatusFunction(vitalName: WebVital): string {
  97. const vitalThreshold = webVitalMeh[vitalName];
  98. const statusFunction = `compare_numeric_aggregate(${getAggregateAlias(
  99. `p75(${vitalName})`
  100. )},greater,${vitalThreshold})`;
  101. return statusFunction;
  102. }
  103. export const vitalMap: Partial<Record<WebVital, string>> = {
  104. [WebVital.FCP]: t('First Contentful Paint'),
  105. [WebVital.CLS]: t('Cumulative Layout Shift'),
  106. [WebVital.FID]: t('First Input Delay'),
  107. [WebVital.LCP]: t('Largest Contentful Paint'),
  108. };
  109. export const vitalChartTitleMap = vitalMap;
  110. export const vitalDescription: Partial<Record<WebVital, string>> = {
  111. [WebVital.FCP]: t(
  112. 'First Contentful Paint (FCP) measures the amount of time the first content takes to render in the viewport. Like FP, this could also show up in any form from the document object model (DOM), such as images, SVGs, or text blocks. At the moment, there is support for FCP in the following browsers:'
  113. ),
  114. [WebVital.CLS]: t(
  115. 'Cumulative Layout Shift (CLS) is the sum of individual layout shift scores for every unexpected element shift during the rendering process. Imagine navigating to an article and trying to click a link before the page finishes loading. Before your cursor even gets there, the link may have shifted down due to an image rendering. Rather than using duration for this Web Vital, the CLS score represents the degree of disruptive and visually unstable shifts. At the moment, there is support for CLS in the following browsers:'
  116. ),
  117. [WebVital.FID]: t(
  118. 'First Input Delay (FID) measures the response time when the user tries to interact with the viewport. Actions maybe include clicking a button, link or other custom Javascript controller. It is key in helping the user determine if a page is usable or not. At the moment, there is support for FID in the following browsers:'
  119. ),
  120. [WebVital.LCP]: t(
  121. 'Largest Contentful Paint (LCP) measures the render time for the largest content to appear in the viewport. This may be in any form from the document object model (DOM), such as images, SVGs, or text blocks. It’s the largest pixel area in the viewport, thus most visually defining. LCP helps developers understand how long it takes to see the main content on the page. At the moment, there is support for LCP in the following browsers:'
  122. ),
  123. [WebVital.TTFB]: t(
  124. 'Time to First Byte (TTFB) is a foundational metric for measuring connection setup time and web server responsiveness in both the lab and the field. It helps identify when a web server is too slow to respond to requests. In the case of navigation requests—that is, requests for an HTML document—it precedes every other meaningful loading performance metric. At the moment, there is support for TTFB in the following browsers:'
  125. ),
  126. };
  127. export const vitalAbbreviations: Partial<Record<WebVital, string>> = {
  128. [WebVital.FCP]: 'FCP',
  129. [WebVital.CLS]: 'CLS',
  130. [WebVital.FID]: 'FID',
  131. [WebVital.LCP]: 'LCP',
  132. };
  133. export const vitalAlertTypes: Partial<Record<WebVital, AlertType>> = {
  134. [WebVital.FCP]: 'custom_transactions',
  135. [WebVital.CLS]: 'cls',
  136. [WebVital.FID]: 'fid',
  137. [WebVital.LCP]: 'lcp',
  138. };
  139. export function getMaxOfSeries(series: Series[]) {
  140. let max = -Infinity;
  141. for (const {data} of series) {
  142. for (const point of data) {
  143. max = Math.max(max, point.value);
  144. }
  145. }
  146. return max;
  147. }
  148. export const vitalSupportedBrowsers: Partial<Record<WebVital, Browser[]>> = {
  149. [WebVital.LCP]: [Browser.CHROME, Browser.EDGE, Browser.OPERA, Browser.FIREFOX],
  150. [WebVital.FID]: [
  151. Browser.CHROME,
  152. Browser.EDGE,
  153. Browser.OPERA,
  154. Browser.FIREFOX,
  155. Browser.SAFARI,
  156. Browser.IE,
  157. ],
  158. [WebVital.CLS]: [Browser.CHROME, Browser.EDGE, Browser.OPERA],
  159. [WebVital.FP]: [Browser.CHROME, Browser.EDGE, Browser.OPERA],
  160. [WebVital.FCP]: [
  161. Browser.CHROME,
  162. Browser.EDGE,
  163. Browser.OPERA,
  164. Browser.FIREFOX,
  165. Browser.SAFARI,
  166. ],
  167. [WebVital.TTFB]: [
  168. Browser.CHROME,
  169. Browser.EDGE,
  170. Browser.OPERA,
  171. Browser.FIREFOX,
  172. Browser.SAFARI,
  173. Browser.IE,
  174. ],
  175. [WebVital.INP]: [Browser.CHROME, Browser.EDGE, Browser.OPERA],
  176. };
  177. export function getVitalChartDefinitions({
  178. theme,
  179. location,
  180. vital,
  181. yAxis,
  182. }: {
  183. location: Location;
  184. theme: Theme;
  185. vital: string;
  186. yAxis: string;
  187. }) {
  188. const utc = decodeScalar(location.query.utc) !== 'false';
  189. const vitalPoor = webVitalPoor[vital];
  190. const vitalMeh = webVitalMeh[vital];
  191. const legend = {
  192. right: 10,
  193. top: 0,
  194. selected: getSeriesSelection(location),
  195. };
  196. const chartOptions: Omit<LineChartProps, 'series'> = {
  197. grid: {
  198. left: '5px',
  199. right: '10px',
  200. top: '35px',
  201. bottom: '0px',
  202. },
  203. seriesOptions: {
  204. showSymbol: false,
  205. },
  206. tooltip: {
  207. trigger: 'axis',
  208. valueFormatter: (value: number, seriesName?: string) =>
  209. tooltipFormatter(
  210. value,
  211. aggregateOutputType(vital === WebVital.CLS ? seriesName : yAxis)
  212. ),
  213. },
  214. yAxis: {
  215. min: 0,
  216. max: vitalPoor,
  217. axisLabel: {
  218. color: theme.chartLabel,
  219. showMaxLabel: false,
  220. // coerces the axis to be time based
  221. formatter: (value: number) =>
  222. axisLabelFormatter(value, aggregateOutputType(yAxis)),
  223. },
  224. },
  225. };
  226. const markLines = [
  227. {
  228. seriesName: 'Thresholds',
  229. type: 'line' as const,
  230. data: [],
  231. markLine: MarkLine({
  232. silent: true,
  233. lineStyle: {
  234. color: theme.red300,
  235. type: 'dashed',
  236. width: 1.5,
  237. },
  238. label: {
  239. show: true,
  240. position: 'insideEndTop',
  241. formatter: t('Poor'),
  242. },
  243. data: [
  244. {
  245. yAxis: vitalPoor,
  246. } as any, // TODO(ts): date on this type is likely incomplete (needs @types/echarts@4.6.2)
  247. ],
  248. }),
  249. },
  250. {
  251. seriesName: 'Thresholds',
  252. type: 'line' as const,
  253. data: [],
  254. markLine: MarkLine({
  255. silent: true,
  256. lineStyle: {
  257. color: theme.yellow300,
  258. type: 'dashed',
  259. width: 1.5,
  260. },
  261. label: {
  262. show: true,
  263. position: 'insideEndTop',
  264. formatter: t('Meh'),
  265. },
  266. data: [
  267. {
  268. yAxis: vitalMeh,
  269. } as any, // TODO(ts): date on this type is likely incomplete (needs @types/echarts@4.6.2)
  270. ],
  271. }),
  272. },
  273. ];
  274. return {
  275. vitalPoor,
  276. vitalMeh,
  277. legend,
  278. chartOptions,
  279. markLines,
  280. utc,
  281. };
  282. }