index.tsx 13 KB

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