utils.tsx 12 KB

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