utils.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import {ASAP} from 'downsample/methods/ASAP';
  2. import {Location} from 'history';
  3. import moment from 'moment';
  4. import {getInterval} from 'sentry/components/charts/utils';
  5. import {t} from 'sentry/locale';
  6. import {Project} from 'sentry/types';
  7. import {Series, SeriesDataUnit} from 'sentry/types/echarts';
  8. import EventView from 'sentry/utils/discover/eventView';
  9. import {
  10. AggregationKeyWithAlias,
  11. Field,
  12. generateFieldAsString,
  13. Sort,
  14. } from 'sentry/utils/discover/fields';
  15. import {decodeScalar} from 'sentry/utils/queryString';
  16. import theme from 'sentry/utils/theme';
  17. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  18. import {platformToPerformanceType, PROJECT_PERFORMANCE_TYPE} from '../utils';
  19. import {
  20. NormalizedTrendsTransaction,
  21. TrendChangeType,
  22. TrendColumnField,
  23. TrendFunction,
  24. TrendFunctionField,
  25. TrendParameter,
  26. TrendsTransaction,
  27. TrendView,
  28. } from './types';
  29. export const DEFAULT_TRENDS_STATS_PERIOD = '14d';
  30. export const DEFAULT_MAX_DURATION = '15min';
  31. export const TRENDS_FUNCTIONS: TrendFunction[] = [
  32. {
  33. label: 'p50',
  34. field: TrendFunctionField.P50,
  35. alias: 'percentile_range',
  36. legendLabel: 'p50',
  37. },
  38. {
  39. label: 'p75',
  40. field: TrendFunctionField.P75,
  41. alias: 'percentile_range',
  42. legendLabel: 'p75',
  43. },
  44. {
  45. label: 'p95',
  46. field: TrendFunctionField.P95,
  47. alias: 'percentile_range',
  48. legendLabel: 'p95',
  49. },
  50. {
  51. label: 'p99',
  52. field: TrendFunctionField.P99,
  53. alias: 'percentile_range',
  54. legendLabel: 'p99',
  55. },
  56. {
  57. label: 'average',
  58. field: TrendFunctionField.AVG,
  59. alias: 'avg_range',
  60. legendLabel: 'average',
  61. },
  62. ];
  63. export const TRENDS_PARAMETERS: TrendParameter[] = [
  64. {
  65. label: 'Duration',
  66. column: TrendColumnField.DURATION,
  67. },
  68. {
  69. label: 'LCP',
  70. column: TrendColumnField.LCP,
  71. },
  72. {
  73. label: 'FCP',
  74. column: TrendColumnField.FCP,
  75. },
  76. {
  77. label: 'FID',
  78. column: TrendColumnField.FID,
  79. },
  80. {
  81. label: 'CLS',
  82. column: TrendColumnField.CLS,
  83. },
  84. {
  85. label: 'Spans (http)',
  86. column: TrendColumnField.SPANS_HTTP,
  87. },
  88. {
  89. label: 'Spans (db)',
  90. column: TrendColumnField.SPANS_DB,
  91. },
  92. {
  93. label: 'Spans (browser)',
  94. column: TrendColumnField.SPANS_BROWSER,
  95. },
  96. {
  97. label: 'Spans (resource)',
  98. column: TrendColumnField.SPANS_RESOURCE,
  99. },
  100. ];
  101. export const trendToColor = {
  102. [TrendChangeType.IMPROVED]: {
  103. lighter: theme.green200,
  104. default: theme.green300,
  105. },
  106. [TrendChangeType.REGRESSION]: {
  107. lighter: theme.red200,
  108. default: theme.red300,
  109. },
  110. };
  111. export const trendSelectedQueryKeys = {
  112. [TrendChangeType.IMPROVED]: 'improvedSelected',
  113. [TrendChangeType.REGRESSION]: 'regressionSelected',
  114. };
  115. export const trendUnselectedSeries = {
  116. [TrendChangeType.IMPROVED]: 'improvedUnselectedSeries',
  117. [TrendChangeType.REGRESSION]: 'regressionUnselectedSeries',
  118. };
  119. export const trendCursorNames = {
  120. [TrendChangeType.IMPROVED]: 'improvedCursor',
  121. [TrendChangeType.REGRESSION]: 'regressionCursor',
  122. };
  123. export function resetCursors() {
  124. const cursors = {};
  125. Object.values(trendCursorNames).forEach(cursor => (cursors[cursor] = undefined)); // Resets both cursors
  126. return cursors;
  127. }
  128. export function getCurrentTrendFunction(
  129. location: Location,
  130. _trendFunctionField?: TrendFunctionField
  131. ): TrendFunction {
  132. const trendFunctionField =
  133. _trendFunctionField ?? decodeScalar(location?.query?.trendFunction);
  134. const trendFunction = TRENDS_FUNCTIONS.find(({field}) => field === trendFunctionField);
  135. return trendFunction || TRENDS_FUNCTIONS[0];
  136. }
  137. function getDefaultTrendParameter(
  138. projects: Project[],
  139. projectIds: Readonly<number[]>
  140. ): TrendParameter {
  141. const performanceType = platformToPerformanceType(projects, projectIds);
  142. const trendParameter = performanceTypeToTrendParameterLabel(performanceType);
  143. return trendParameter;
  144. }
  145. export function getCurrentTrendParameter(
  146. location: Location,
  147. projects: Project[],
  148. projectIds: Readonly<number[]>
  149. ): TrendParameter {
  150. const trendParameterLabel = decodeScalar(location?.query?.trendParameter);
  151. const trendParameter = TRENDS_PARAMETERS.find(
  152. ({label}) => label === trendParameterLabel
  153. );
  154. if (trendParameter) {
  155. return trendParameter;
  156. }
  157. const defaultTrendParameter = getDefaultTrendParameter(projects, projectIds);
  158. return defaultTrendParameter;
  159. }
  160. export function performanceTypeToTrendParameterLabel(
  161. performanceType: PROJECT_PERFORMANCE_TYPE
  162. ): TrendParameter {
  163. switch (performanceType) {
  164. case PROJECT_PERFORMANCE_TYPE.FRONTEND:
  165. return {
  166. label: 'LCP',
  167. column: TrendColumnField.LCP,
  168. };
  169. case PROJECT_PERFORMANCE_TYPE.ANY:
  170. case PROJECT_PERFORMANCE_TYPE.BACKEND:
  171. case PROJECT_PERFORMANCE_TYPE.FRONTEND_OTHER:
  172. default:
  173. return {
  174. label: 'Duration',
  175. column: TrendColumnField.DURATION,
  176. };
  177. }
  178. }
  179. export function generateTrendFunctionAsString(
  180. trendFunction: TrendFunctionField,
  181. trendParameter: string
  182. ): string {
  183. return generateFieldAsString({
  184. kind: 'function',
  185. function: [
  186. trendFunction as AggregationKeyWithAlias,
  187. trendParameter,
  188. undefined,
  189. undefined,
  190. ],
  191. });
  192. }
  193. export function transformDeltaSpread(from: number, to: number) {
  194. const fromSeconds = from / 1000;
  195. const toSeconds = to / 1000;
  196. const showDigits = from > 1000 || to > 1000 || from < 10 || to < 10; // Show digits consistently if either has them
  197. return {fromSeconds, toSeconds, showDigits};
  198. }
  199. export function getTrendProjectId(
  200. trend: NormalizedTrendsTransaction,
  201. projects?: Project[]
  202. ): string | undefined {
  203. if (!trend.project || !projects) {
  204. return undefined;
  205. }
  206. const transactionProject = projects.find(project => project.slug === trend.project);
  207. return transactionProject?.id;
  208. }
  209. export function modifyTrendView(
  210. trendView: TrendView,
  211. location: Location,
  212. trendsType: TrendChangeType,
  213. projects: Project[],
  214. isProjectOnly?: boolean
  215. ) {
  216. const trendFunction = getCurrentTrendFunction(location);
  217. const trendParameter = getCurrentTrendParameter(location, projects, trendView.project);
  218. const transactionField = isProjectOnly ? [] : ['transaction'];
  219. const fields = [...transactionField, 'project'].map(field => ({
  220. field,
  221. })) as Field[];
  222. const trendSort = {
  223. field: 'trend_percentage()',
  224. kind: 'asc',
  225. } as Sort;
  226. trendView.trendType = trendsType;
  227. if (trendsType === TrendChangeType.REGRESSION) {
  228. trendSort.kind = 'desc';
  229. }
  230. if (trendFunction && trendParameter) {
  231. trendView.trendFunction = generateTrendFunctionAsString(
  232. trendFunction.field,
  233. trendParameter.column
  234. );
  235. }
  236. trendView.query = getLimitTransactionItems(trendView.query);
  237. trendView.interval = getQueryInterval(location, trendView);
  238. trendView.sorts = [trendSort];
  239. trendView.fields = fields;
  240. }
  241. export function modifyTrendsViewDefaultPeriod(eventView: EventView, location: Location) {
  242. const {query} = location;
  243. const hasStartAndEnd = query.start && query.end;
  244. if (!query.statsPeriod && !hasStartAndEnd) {
  245. eventView.statsPeriod = DEFAULT_TRENDS_STATS_PERIOD;
  246. }
  247. return eventView;
  248. }
  249. function getQueryInterval(location: Location, eventView: TrendView) {
  250. const intervalFromQueryParam = decodeScalar(location?.query?.interval);
  251. const {start, end, statsPeriod} = eventView;
  252. const datetimeSelection = {
  253. start: start || null,
  254. end: end || null,
  255. period: statsPeriod,
  256. };
  257. const intervalFromSmoothing = getInterval(datetimeSelection, 'high');
  258. return intervalFromQueryParam || intervalFromSmoothing;
  259. }
  260. export function transformValueDelta(value: number, trendType: TrendChangeType) {
  261. const absoluteValue = Math.abs(value);
  262. const changeLabel =
  263. trendType === TrendChangeType.REGRESSION ? t('slower') : t('faster');
  264. const seconds = absoluteValue / 1000;
  265. const fixedDigits = absoluteValue > 1000 || absoluteValue < 10 ? 1 : 0;
  266. return {seconds, fixedDigits, changeLabel};
  267. }
  268. /**
  269. * This will normalize the trends transactions while the current trend function and current data are out of sync
  270. * To minimize extra renders with missing results.
  271. */
  272. export function normalizeTrends(
  273. data: Array<TrendsTransaction>
  274. ): Array<NormalizedTrendsTransaction> {
  275. const received_at = moment(); // Adding the received time for the transaction so calls to get baseline always line up with the transaction
  276. return data.map(row => {
  277. return {
  278. ...row,
  279. received_at,
  280. transaction: row.transaction,
  281. } as NormalizedTrendsTransaction;
  282. });
  283. }
  284. export function getSelectedQueryKey(trendChangeType: TrendChangeType) {
  285. return trendSelectedQueryKeys[trendChangeType];
  286. }
  287. export function getUnselectedSeries(trendChangeType: TrendChangeType) {
  288. return trendUnselectedSeries[trendChangeType];
  289. }
  290. export function movingAverage(data, index, size) {
  291. return (
  292. data
  293. .slice(index - size, index)
  294. .map(a => a.value)
  295. .reduce((a, b) => a + b, 0) / size
  296. );
  297. }
  298. /**
  299. * This function applies defaults for trend and count percentage, and adds the confidence limit to the query
  300. */
  301. function getLimitTransactionItems(query: string) {
  302. const limitQuery = new MutableSearch(query);
  303. if (!limitQuery.hasFilter('count_percentage()')) {
  304. limitQuery.addFilterValues('count_percentage()', ['>0.25', '<4']);
  305. }
  306. if (!limitQuery.hasFilter('trend_percentage()')) {
  307. limitQuery.addFilterValues('trend_percentage()', ['>0%']);
  308. }
  309. if (!limitQuery.hasFilter('confidence()')) {
  310. limitQuery.addFilterValues('confidence()', ['>6']);
  311. }
  312. return limitQuery.formatString();
  313. }
  314. export const smoothTrend = (data: [number, number][], resolution = 100) => {
  315. return ASAP(data, resolution);
  316. };
  317. export const replaceSeriesName = (seriesName: string) => {
  318. return ['p50', 'p75'].find(aggregate => seriesName.includes(aggregate));
  319. };
  320. export const replaceSmoothedSeriesName = (seriesName: string) => {
  321. return `Smoothed ${['p50', 'p75'].find(aggregate => seriesName.includes(aggregate))}`;
  322. };
  323. export function transformEventStatsSmoothed(data?: Series[], seriesName?: string) {
  324. let minValue = Number.MAX_SAFE_INTEGER;
  325. let maxValue = 0;
  326. if (!data) {
  327. return {
  328. maxValue,
  329. minValue,
  330. smoothedResults: undefined,
  331. };
  332. }
  333. const smoothedResults: Series[] = [];
  334. for (const current of data) {
  335. const currentData = current.data;
  336. const resultData: SeriesDataUnit[] = [];
  337. const smoothed = smoothTrend(
  338. currentData.map(({name, value}) => [Number(name), value])
  339. );
  340. for (let i = 0; i < smoothed.length; i++) {
  341. const point = smoothed[i] as any;
  342. const value = point.y;
  343. resultData.push({
  344. name: point.x,
  345. value,
  346. });
  347. if (!isNaN(value)) {
  348. const rounded = Math.round(value);
  349. minValue = Math.min(rounded, minValue);
  350. maxValue = Math.max(rounded, maxValue);
  351. }
  352. }
  353. smoothedResults.push({
  354. seriesName: seriesName || current.seriesName || 'Current',
  355. data: resultData,
  356. lineStyle: current.lineStyle,
  357. color: current.color,
  358. });
  359. }
  360. return {
  361. minValue,
  362. maxValue,
  363. smoothedResults,
  364. };
  365. }