utils.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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 {Project} from 'sentry/types';
  7. import {Series, SeriesDataUnit} from 'sentry/types/echarts';
  8. import EventView from 'sentry/utils/discover/eventView';
  9. import {
  10. AggregationKey,
  11. Field,
  12. generateFieldAsString,
  13. Sort,
  14. } from 'sentry/utils/discover/fields';
  15. import {TransactionMetric} from 'sentry/utils/metrics/fields';
  16. import {decodeScalar} from 'sentry/utils/queryString';
  17. import theme from 'sentry/utils/theme';
  18. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  19. import {
  20. NormalizedTrendsTransaction,
  21. TrendChangeType,
  22. TrendColumnField,
  23. TrendFunction,
  24. TrendFunctionField,
  25. TrendParameter,
  26. TrendsTransaction,
  27. TrendView,
  28. } from './types';
  29. export const trendParameterToMetricsField: Record<string, TransactionMetric> = {
  30. [TrendColumnField.DURATION]: TransactionMetric.SENTRY_TRANSACTIONS_TRANSACTION_DURATION,
  31. [TrendColumnField.LCP]: TransactionMetric.SENTRY_TRANSACTIONS_MEASUREMENTS_LCP,
  32. [TrendColumnField.FCP]: TransactionMetric.SENTRY_TRANSACTIONS_MEASUREMENTS_FCP,
  33. [TrendColumnField.FID]: TransactionMetric.SENTRY_TRANSACTIONS_MEASUREMENTS_FID,
  34. [TrendColumnField.CLS]: TransactionMetric.SENTRY_TRANSACTIONS_MEASUREMENTS_CLS,
  35. };
  36. export const DEFAULT_TRENDS_STATS_PERIOD = '14d';
  37. export const DEFAULT_MAX_DURATION = '15min';
  38. export const TRENDS_FUNCTIONS: TrendFunction[] = [
  39. {
  40. label: 'p50',
  41. field: TrendFunctionField.P50,
  42. alias: 'percentile_range',
  43. legendLabel: 'p50',
  44. },
  45. {
  46. label: 'p75',
  47. field: TrendFunctionField.P75,
  48. alias: 'percentile_range',
  49. legendLabel: 'p75',
  50. },
  51. {
  52. label: 'p95',
  53. field: TrendFunctionField.P95,
  54. alias: 'percentile_range',
  55. legendLabel: 'p95',
  56. },
  57. {
  58. label: 'p99',
  59. field: TrendFunctionField.P99,
  60. alias: 'percentile_range',
  61. legendLabel: 'p99',
  62. },
  63. {
  64. label: 'average',
  65. field: TrendFunctionField.AVG,
  66. alias: 'avg_range',
  67. legendLabel: 'average',
  68. },
  69. ];
  70. export const TRENDS_PARAMETERS: TrendParameter[] = [
  71. {
  72. label: 'Duration',
  73. column: TrendColumnField.DURATION,
  74. },
  75. {
  76. label: 'LCP',
  77. column: TrendColumnField.LCP,
  78. },
  79. {
  80. label: 'FCP',
  81. column: TrendColumnField.FCP,
  82. },
  83. {
  84. label: 'FID',
  85. column: TrendColumnField.FID,
  86. },
  87. {
  88. label: 'CLS',
  89. column: TrendColumnField.CLS,
  90. },
  91. {
  92. label: 'Spans (http)',
  93. column: TrendColumnField.SPANS_HTTP,
  94. },
  95. {
  96. label: 'Spans (db)',
  97. column: TrendColumnField.SPANS_DB,
  98. },
  99. {
  100. label: 'Spans (browser)',
  101. column: TrendColumnField.SPANS_BROWSER,
  102. },
  103. {
  104. label: 'Spans (resource)',
  105. column: TrendColumnField.SPANS_RESOURCE,
  106. },
  107. ];
  108. export const trendToColor = {
  109. [TrendChangeType.IMPROVED]: {
  110. lighter: theme.green200,
  111. default: theme.green300,
  112. },
  113. [TrendChangeType.REGRESSION]: {
  114. lighter: theme.red200,
  115. default: theme.red300,
  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. export function getCurrentTrendParameter(location: Location): TrendParameter {
  145. const trendParameterLabel = decodeScalar(location?.query?.trendParameter);
  146. const trendParameter = TRENDS_PARAMETERS.find(
  147. ({label}) => label === trendParameterLabel
  148. );
  149. return trendParameter || TRENDS_PARAMETERS[0];
  150. }
  151. export function generateTrendFunctionAsString(
  152. trendFunction: TrendFunctionField,
  153. trendParameter: string
  154. ): string {
  155. return generateFieldAsString({
  156. kind: 'function',
  157. function: [trendFunction as AggregationKey, trendParameter, undefined, undefined],
  158. });
  159. }
  160. export function transformDeltaSpread(from: number, to: number) {
  161. const fromSeconds = from / 1000;
  162. const toSeconds = to / 1000;
  163. const showDigits = from > 1000 || to > 1000 || from < 10 || to < 10; // Show digits consistently if either has them
  164. return {fromSeconds, toSeconds, showDigits};
  165. }
  166. export function getTrendProjectId(
  167. trend: NormalizedTrendsTransaction,
  168. projects?: Project[]
  169. ): string | undefined {
  170. if (!trend.project || !projects) {
  171. return undefined;
  172. }
  173. const transactionProject = projects.find(project => project.slug === trend.project);
  174. return transactionProject?.id;
  175. }
  176. export function modifyTrendView(
  177. trendView: TrendView,
  178. location: Location,
  179. trendsType: TrendChangeType,
  180. isProjectOnly?: boolean
  181. ) {
  182. const trendFunction = getCurrentTrendFunction(location);
  183. const trendParameter = getCurrentTrendParameter(location);
  184. const transactionField = isProjectOnly ? [] : ['transaction'];
  185. const fields = [...transactionField, 'project'].map(field => ({
  186. field,
  187. })) as Field[];
  188. const trendSort = {
  189. field: 'trend_percentage()',
  190. kind: 'asc',
  191. } as Sort;
  192. trendView.trendType = trendsType;
  193. if (trendsType === TrendChangeType.REGRESSION) {
  194. trendSort.kind = 'desc';
  195. }
  196. if (trendFunction && trendParameter) {
  197. trendView.trendFunction = generateTrendFunctionAsString(
  198. trendFunction.field,
  199. trendParameter.column
  200. );
  201. }
  202. trendView.query = getLimitTransactionItems(trendView.query);
  203. trendView.interval = getQueryInterval(location, trendView);
  204. trendView.sorts = [trendSort];
  205. trendView.fields = fields;
  206. }
  207. export function modifyTrendsViewDefaultPeriod(eventView: EventView, location: Location) {
  208. const {query} = location;
  209. const hasStartAndEnd = query.start && query.end;
  210. if (!query.statsPeriod && !hasStartAndEnd) {
  211. eventView.statsPeriod = DEFAULT_TRENDS_STATS_PERIOD;
  212. }
  213. return eventView;
  214. }
  215. function getQueryInterval(location: Location, eventView: TrendView) {
  216. const intervalFromQueryParam = decodeScalar(location?.query?.interval);
  217. const {start, end, statsPeriod} = eventView;
  218. const datetimeSelection = {
  219. start: start || null,
  220. end: end || null,
  221. period: statsPeriod,
  222. };
  223. const intervalFromSmoothing = getInterval(datetimeSelection, 'high');
  224. return intervalFromQueryParam || intervalFromSmoothing;
  225. }
  226. export function transformValueDelta(value: number, trendType: TrendChangeType) {
  227. const absoluteValue = Math.abs(value);
  228. const changeLabel =
  229. trendType === TrendChangeType.REGRESSION ? t('slower') : t('faster');
  230. const seconds = absoluteValue / 1000;
  231. const fixedDigits = absoluteValue > 1000 || absoluteValue < 10 ? 1 : 0;
  232. return {seconds, fixedDigits, changeLabel};
  233. }
  234. /**
  235. * This will normalize the trends transactions while the current trend function and current data are out of sync
  236. * To minimize extra renders with missing results.
  237. */
  238. export function normalizeTrends(
  239. data: Array<TrendsTransaction>
  240. ): Array<NormalizedTrendsTransaction> {
  241. const received_at = moment(); // Adding the received time for the transaction so calls to get baseline always line up with the transaction
  242. return data.map(row => {
  243. return {
  244. ...row,
  245. received_at,
  246. transaction: row.transaction,
  247. } as NormalizedTrendsTransaction;
  248. });
  249. }
  250. export function getSelectedQueryKey(trendChangeType: TrendChangeType) {
  251. return trendSelectedQueryKeys[trendChangeType];
  252. }
  253. export function getUnselectedSeries(trendChangeType: TrendChangeType) {
  254. return trendUnselectedSeries[trendChangeType];
  255. }
  256. export function movingAverage(data, index, size) {
  257. return (
  258. data
  259. .slice(index - size, index)
  260. .map(a => a.value)
  261. .reduce((a, b) => a + b, 0) / size
  262. );
  263. }
  264. /**
  265. * This function applies defaults for trend and count percentage, and adds the confidence limit to the query
  266. */
  267. function getLimitTransactionItems(query: string) {
  268. const limitQuery = new MutableSearch(query);
  269. if (!limitQuery.hasFilter('count_percentage()')) {
  270. limitQuery.addFilterValues('count_percentage()', ['>0.25', '<4']);
  271. }
  272. if (!limitQuery.hasFilter('trend_percentage()')) {
  273. limitQuery.addFilterValues('trend_percentage()', ['>0%']);
  274. }
  275. if (!limitQuery.hasFilter('confidence()')) {
  276. limitQuery.addFilterValues('confidence()', ['>6']);
  277. }
  278. return limitQuery.formatString();
  279. }
  280. export const smoothTrend = (data: [number, number][], resolution = 100) => {
  281. return ASAP(data, resolution);
  282. };
  283. export const replaceSeriesName = (seriesName: string) => {
  284. return ['p50', 'p75'].find(aggregate => seriesName.includes(aggregate));
  285. };
  286. export const replaceSmoothedSeriesName = (seriesName: string) => {
  287. return `Smoothed ${['p50', 'p75'].find(aggregate => seriesName.includes(aggregate))}`;
  288. };
  289. export function transformEventStatsSmoothed(data?: Series[], seriesName?: string) {
  290. let minValue = Number.MAX_SAFE_INTEGER;
  291. let maxValue = 0;
  292. if (!data) {
  293. return {
  294. maxValue,
  295. minValue,
  296. smoothedResults: undefined,
  297. };
  298. }
  299. const smoothedResults: Series[] = [];
  300. for (const current of data) {
  301. const currentData = current.data;
  302. const resultData: SeriesDataUnit[] = [];
  303. const smoothed = smoothTrend(
  304. currentData.map(({name, value}) => [Number(name), value])
  305. );
  306. for (let i = 0; i < smoothed.length; i++) {
  307. const point = smoothed[i] as any;
  308. const value = point.y;
  309. resultData.push({
  310. name: point.x,
  311. value,
  312. });
  313. if (!isNaN(value)) {
  314. const rounded = Math.round(value);
  315. minValue = Math.min(rounded, minValue);
  316. maxValue = Math.max(rounded, maxValue);
  317. }
  318. }
  319. smoothedResults.push({
  320. seriesName: seriesName || current.seriesName || 'Current',
  321. data: resultData,
  322. lineStyle: current.lineStyle,
  323. color: current.color,
  324. });
  325. }
  326. return {
  327. minValue,
  328. maxValue,
  329. smoothedResults,
  330. };
  331. }