utils.tsx 12 KB

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