utils.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. import {ASAP} from 'downsample/methods/ASAP';
  2. import type {Location} from 'history';
  3. import moment from 'moment';
  4. import {getInterval} from 'sentry/components/charts/utils';
  5. import {wrapQueryInWildcards} from 'sentry/components/performance/searchBar';
  6. import {t} from 'sentry/locale';
  7. import type {Project} from 'sentry/types';
  8. import type {Series, SeriesDataUnit} from 'sentry/types/echarts';
  9. import type EventView from 'sentry/utils/discover/eventView';
  10. import type {AggregationKeyWithAlias, Field, Sort} from 'sentry/utils/discover/fields';
  11. import {generateFieldAsString} from 'sentry/utils/discover/fields';
  12. import {decodeScalar} from 'sentry/utils/queryString';
  13. import theme from 'sentry/utils/theme';
  14. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  15. import {platformToPerformanceType, ProjectPerformanceType} from '../utils';
  16. import type {
  17. NormalizedTrendsTransaction,
  18. TrendFunction,
  19. TrendParameter,
  20. TrendsTransaction,
  21. TrendView,
  22. } from './types';
  23. import {
  24. TrendChangeType,
  25. TrendFunctionField,
  26. TrendParameterColumn,
  27. TrendParameterLabel,
  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: TrendParameterLabel.DURATION,
  66. column: TrendParameterColumn.DURATION,
  67. },
  68. {
  69. label: TrendParameterLabel.LCP,
  70. column: TrendParameterColumn.LCP,
  71. },
  72. {
  73. label: TrendParameterLabel.FCP,
  74. column: TrendParameterColumn.FCP,
  75. },
  76. {
  77. label: TrendParameterLabel.FID,
  78. column: TrendParameterColumn.FID,
  79. },
  80. {
  81. label: TrendParameterLabel.CLS,
  82. column: TrendParameterColumn.CLS,
  83. },
  84. {
  85. label: TrendParameterLabel.SPANS_HTTP,
  86. column: TrendParameterColumn.SPANS_HTTP,
  87. },
  88. {
  89. label: TrendParameterLabel.SPANS_DB,
  90. column: TrendParameterColumn.SPANS_DB,
  91. },
  92. {
  93. label: TrendParameterLabel.SPANS_BROWSER,
  94. column: TrendParameterColumn.SPANS_BROWSER,
  95. },
  96. {
  97. label: TrendParameterLabel.SPANS_RESOURCE,
  98. column: TrendParameterColumn.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. neutral: {
  111. lighter: theme.yellow200,
  112. default: theme.yellow300,
  113. },
  114. // TODO remove this once backend starts sending
  115. // TrendChangeType.IMPROVED as change type
  116. improvement: {
  117. lighter: theme.green200,
  118. default: theme.green300,
  119. },
  120. };
  121. export const trendSelectedQueryKeys = {
  122. [TrendChangeType.IMPROVED]: 'improvedSelected',
  123. [TrendChangeType.REGRESSION]: 'regressionSelected',
  124. };
  125. export const trendUnselectedSeries = {
  126. [TrendChangeType.IMPROVED]: 'improvedUnselectedSeries',
  127. [TrendChangeType.REGRESSION]: 'regressionUnselectedSeries',
  128. };
  129. export const trendCursorNames = {
  130. [TrendChangeType.IMPROVED]: 'improvedCursor',
  131. [TrendChangeType.REGRESSION]: 'regressionCursor',
  132. };
  133. const TOKEN_KEYS_SUPPORTED_IN_METRICS_TRENDS = ['transaction', 'tpm()'];
  134. export function resetCursors() {
  135. const cursors = {};
  136. Object.values(trendCursorNames).forEach(cursor => (cursors[cursor] = undefined)); // Resets both cursors
  137. return cursors;
  138. }
  139. export function getCurrentTrendFunction(
  140. location: Location,
  141. _trendFunctionField?: TrendFunctionField
  142. ): TrendFunction {
  143. const trendFunctionField =
  144. _trendFunctionField ?? decodeScalar(location?.query?.trendFunction);
  145. const trendFunction = TRENDS_FUNCTIONS.find(({field}) => field === trendFunctionField);
  146. return trendFunction || TRENDS_FUNCTIONS[0];
  147. }
  148. function getDefaultTrendParameter(
  149. projects: Project[],
  150. projectIds: Readonly<number[]>
  151. ): TrendParameter {
  152. const performanceType = platformToPerformanceType(projects, projectIds);
  153. const trendParameter = performanceTypeToTrendParameterLabel(performanceType);
  154. return trendParameter;
  155. }
  156. export function getCurrentTrendParameter(
  157. location: Location,
  158. projects: Project[],
  159. projectIds: Readonly<number[]>
  160. ): TrendParameter {
  161. const trendParameterLabel = decodeScalar(location?.query?.trendParameter);
  162. const trendParameter = TRENDS_PARAMETERS.find(
  163. ({label}) => label === trendParameterLabel
  164. );
  165. if (trendParameter) {
  166. return trendParameter;
  167. }
  168. const defaultTrendParameter = getDefaultTrendParameter(projects, projectIds);
  169. return defaultTrendParameter;
  170. }
  171. export function performanceTypeToTrendParameterLabel(
  172. performanceType: ProjectPerformanceType
  173. ): TrendParameter {
  174. switch (performanceType) {
  175. case ProjectPerformanceType.FRONTEND:
  176. return {
  177. label: TrendParameterLabel.LCP,
  178. column: TrendParameterColumn.LCP,
  179. };
  180. case ProjectPerformanceType.ANY:
  181. case ProjectPerformanceType.BACKEND:
  182. case ProjectPerformanceType.FRONTEND_OTHER:
  183. default:
  184. return {
  185. label: TrendParameterLabel.DURATION,
  186. column: TrendParameterColumn.DURATION,
  187. };
  188. }
  189. }
  190. export function generateTrendFunctionAsString(
  191. trendFunction: TrendFunctionField,
  192. trendParameter: string
  193. ): string {
  194. return generateFieldAsString({
  195. kind: 'function',
  196. function: [
  197. trendFunction as AggregationKeyWithAlias,
  198. trendParameter,
  199. undefined,
  200. undefined,
  201. ],
  202. });
  203. }
  204. export function transformDeltaSpread(from: number, to: number) {
  205. const fromSeconds = from / 1000;
  206. const toSeconds = to / 1000;
  207. const showDigits = from > 1000 || to > 1000 || from < 10 || to < 10; // Show digits consistently if either has them
  208. return {fromSeconds, toSeconds, showDigits};
  209. }
  210. export function getTrendProjectId(
  211. trend: NormalizedTrendsTransaction,
  212. projects?: Project[]
  213. ): string | undefined {
  214. if (!trend.project || !projects) {
  215. return undefined;
  216. }
  217. const transactionProject = projects.find(project => project.slug === trend.project);
  218. return transactionProject?.id;
  219. }
  220. export function modifyTrendView(
  221. trendView: TrendView,
  222. location: Location,
  223. trendsType: TrendChangeType,
  224. projects: Project[],
  225. canUseMetricsTrends: boolean = false
  226. ) {
  227. const trendFunction = getCurrentTrendFunction(location);
  228. const trendParameter = getCurrentTrendParameter(location, projects, trendView.project);
  229. const fields = ['transaction', 'project'].map(field => ({
  230. field,
  231. })) as Field[];
  232. const trendSort = {
  233. field: 'trend_percentage()',
  234. kind: 'asc',
  235. } as Sort;
  236. trendView.trendType = trendsType;
  237. if (trendsType === TrendChangeType.REGRESSION) {
  238. trendSort.kind = 'desc';
  239. }
  240. if (trendFunction && trendParameter) {
  241. trendView.trendFunction = generateTrendFunctionAsString(
  242. trendFunction.field,
  243. trendParameter.column
  244. );
  245. }
  246. if (!canUseMetricsTrends) {
  247. trendView.query = getLimitTransactionItems(trendView.query);
  248. } else {
  249. const query = new MutableSearch(trendView.query);
  250. if (query.freeText.length > 0) {
  251. const parsedFreeText = query.freeText.join(' ');
  252. // the query here is a user entered condition, no need to escape it
  253. query.setFilterValues('transaction', [wrapQueryInWildcards(parsedFreeText)], false);
  254. query.freeText = [];
  255. }
  256. query.tokens = query.tokens.filter(
  257. token => token.key && TOKEN_KEYS_SUPPORTED_IN_METRICS_TRENDS.includes(token.key)
  258. );
  259. trendView.query = query.formatString();
  260. }
  261. trendView.interval = getQueryInterval(location, trendView);
  262. trendView.sorts = [trendSort];
  263. trendView.fields = fields;
  264. }
  265. export function modifyTrendsViewDefaultPeriod(eventView: EventView, location: Location) {
  266. const {query} = location;
  267. const hasStartAndEnd = query.start && query.end;
  268. if (!query.statsPeriod && !hasStartAndEnd) {
  269. eventView.statsPeriod = DEFAULT_TRENDS_STATS_PERIOD;
  270. }
  271. return eventView;
  272. }
  273. function getQueryInterval(location: Location, eventView: TrendView) {
  274. const intervalFromQueryParam = decodeScalar(location?.query?.interval);
  275. const {start, end, statsPeriod} = eventView;
  276. const datetimeSelection = {
  277. start: start || null,
  278. end: end || null,
  279. period: statsPeriod,
  280. };
  281. const intervalFromSmoothing = getInterval(datetimeSelection, 'medium');
  282. return intervalFromQueryParam || intervalFromSmoothing;
  283. }
  284. export function transformValueDelta(value: number, trendType: TrendChangeType) {
  285. const absoluteValue = Math.abs(value);
  286. const changeLabel =
  287. trendType === TrendChangeType.REGRESSION ? t('slower') : t('faster');
  288. const seconds = absoluteValue / 1000;
  289. const fixedDigits = absoluteValue > 1000 || absoluteValue < 10 ? 1 : 0;
  290. return {seconds, fixedDigits, changeLabel};
  291. }
  292. /**
  293. * This will normalize the trends transactions while the current trend function and current data are out of sync
  294. * To minimize extra renders with missing results.
  295. */
  296. export function normalizeTrends(
  297. data: Array<TrendsTransaction>
  298. ): Array<NormalizedTrendsTransaction> {
  299. const received_at = moment(); // Adding the received time for the transaction so calls to get baseline always line up with the transaction
  300. return data.map(row => {
  301. return {
  302. ...row,
  303. received_at,
  304. transaction: row.transaction,
  305. } as NormalizedTrendsTransaction;
  306. });
  307. }
  308. export function getSelectedQueryKey(trendChangeType: TrendChangeType) {
  309. return trendSelectedQueryKeys[trendChangeType];
  310. }
  311. export function getUnselectedSeries(trendChangeType: TrendChangeType) {
  312. return trendUnselectedSeries[trendChangeType];
  313. }
  314. export function movingAverage(data, index, size) {
  315. return (
  316. data
  317. .slice(index - size, index)
  318. .map(a => a.value)
  319. .reduce((a, b) => a + b, 0) / size
  320. );
  321. }
  322. /**
  323. * This function applies defaults for trend and count percentage, and adds the confidence limit to the query
  324. */
  325. function getLimitTransactionItems(query: string) {
  326. const limitQuery = new MutableSearch(query);
  327. if (!limitQuery.hasFilter('count_percentage()')) {
  328. limitQuery.addFilterValues('count_percentage()', ['>0.25', '<4']);
  329. }
  330. if (!limitQuery.hasFilter('trend_percentage()')) {
  331. limitQuery.addFilterValues('trend_percentage()', ['>0%']);
  332. }
  333. if (!limitQuery.hasFilter('confidence()')) {
  334. limitQuery.addFilterValues('confidence()', ['>6']);
  335. }
  336. return limitQuery.formatString();
  337. }
  338. export const smoothTrend = (data: [number, number][], resolution = 100) => {
  339. return ASAP(data, resolution);
  340. };
  341. export const replaceSeriesName = (seriesName: string) => {
  342. return ['p50', 'p75'].find(aggregate => seriesName.includes(aggregate));
  343. };
  344. export const replaceSmoothedSeriesName = (seriesName: string) => {
  345. return `Smoothed ${['p50', 'p75'].find(aggregate => seriesName.includes(aggregate))}`;
  346. };
  347. export function transformEventStatsSmoothed(data?: Series[], seriesName?: string) {
  348. let minValue = Number.MAX_SAFE_INTEGER;
  349. let maxValue = 0;
  350. if (!data) {
  351. return {
  352. maxValue,
  353. minValue,
  354. smoothedResults: undefined,
  355. };
  356. }
  357. const smoothedResults: Series[] = [];
  358. for (const current of data) {
  359. const currentData = current.data;
  360. const resultData: SeriesDataUnit[] = [];
  361. const smoothed = smoothTrend(
  362. currentData.map(({name, value}) => [Number(name), value])
  363. );
  364. for (let i = 0; i < smoothed.length; i++) {
  365. const point = smoothed[i] as any;
  366. const value = point.y;
  367. resultData.push({
  368. name: point.x,
  369. value,
  370. });
  371. if (!isNaN(value)) {
  372. const rounded = Math.round(value);
  373. minValue = Math.min(rounded, minValue);
  374. maxValue = Math.max(rounded, maxValue);
  375. }
  376. }
  377. smoothedResults.push({
  378. seriesName: seriesName || current.seriesName || 'Current',
  379. data: resultData,
  380. lineStyle: current.lineStyle,
  381. color: current.color,
  382. });
  383. }
  384. return {
  385. minValue,
  386. maxValue,
  387. smoothedResults,
  388. };
  389. }
  390. export function modifyTransactionNameTrendsQuery(trendView: TrendView) {
  391. const query = new MutableSearch(trendView.query);
  392. query.setFilterValues('tpm()', ['>0.1']);
  393. trendView.query = query.formatString();
  394. }
  395. export function getTopTrendingEvents(location: Location) {
  396. return decodeScalar(location?.query?.topEvents);
  397. }