utils.tsx 11 KB

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