utils.tsx 9.8 KB

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