utils.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import type {LegendComponentOption} from 'echarts';
  2. import type {Location} from 'history';
  3. import moment from 'moment';
  4. import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
  5. import {EventsStats, MultiSeriesEventsStats, PageFilters} from 'sentry/types';
  6. import {defined, escape} from 'sentry/utils';
  7. import {parsePeriodToHours} from 'sentry/utils/dates';
  8. import type {TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
  9. import {decodeList} from 'sentry/utils/queryString';
  10. const DEFAULT_TRUNCATE_LENGTH = 80;
  11. // In minutes
  12. export const SIXTY_DAYS = 86400;
  13. export const THIRTY_DAYS = 43200;
  14. export const TWO_WEEKS = 20160;
  15. export const ONE_WEEK = 10080;
  16. export const TWENTY_FOUR_HOURS = 1440;
  17. export const SIX_HOURS = 360;
  18. export const ONE_HOUR = 60;
  19. /**
  20. * If there are more releases than this number we hide "Releases" series by default
  21. */
  22. export const RELEASE_LINES_THRESHOLD = 50;
  23. export type DateTimeObject = Partial<PageFilters['datetime']>;
  24. export function truncationFormatter(
  25. value: string,
  26. truncate: number | boolean | undefined
  27. ): string {
  28. if (!truncate) {
  29. return escape(value);
  30. }
  31. const truncationLength =
  32. truncate && typeof truncate === 'number' ? truncate : DEFAULT_TRUNCATE_LENGTH;
  33. const truncated =
  34. value.length > truncationLength ? value.substring(0, truncationLength) + '…' : value;
  35. return escape(truncated);
  36. }
  37. /**
  38. * Use a shorter interval if the time difference is <= 24 hours.
  39. */
  40. export function useShortInterval(datetimeObj: DateTimeObject): boolean {
  41. const diffInMinutes = getDiffInMinutes(datetimeObj);
  42. return diffInMinutes <= TWENTY_FOUR_HOURS;
  43. }
  44. export type Fidelity = 'high' | 'medium' | 'low';
  45. export function getInterval(datetimeObj: DateTimeObject, fidelity: Fidelity = 'medium') {
  46. const diffInMinutes = getDiffInMinutes(datetimeObj);
  47. if (diffInMinutes >= SIXTY_DAYS) {
  48. // Greater than or equal to 60 days
  49. if (fidelity === 'high') {
  50. return '4h';
  51. }
  52. if (fidelity === 'medium') {
  53. return '1d';
  54. }
  55. return '2d';
  56. }
  57. if (diffInMinutes >= THIRTY_DAYS) {
  58. // Greater than or equal to 30 days
  59. if (fidelity === 'high') {
  60. return '1h';
  61. }
  62. if (fidelity === 'medium') {
  63. return '4h';
  64. }
  65. return '1d';
  66. }
  67. if (diffInMinutes >= TWO_WEEKS) {
  68. if (fidelity === 'high') {
  69. return '30m';
  70. }
  71. if (fidelity === 'medium') {
  72. return '1h';
  73. }
  74. return '12h';
  75. }
  76. if (diffInMinutes > TWENTY_FOUR_HOURS) {
  77. // Greater than 24 hours
  78. if (fidelity === 'high') {
  79. return '30m';
  80. }
  81. if (fidelity === 'medium') {
  82. return '1h';
  83. }
  84. return '6h';
  85. }
  86. if (diffInMinutes > ONE_HOUR) {
  87. // Between 1 hour and 24 hours
  88. if (fidelity === 'high') {
  89. return '5m';
  90. }
  91. if (fidelity === 'medium') {
  92. return '15m';
  93. }
  94. return '1h';
  95. }
  96. // Less than or equal to 1 hour
  97. if (fidelity === 'high') {
  98. return '1m';
  99. }
  100. if (fidelity === 'medium') {
  101. return '5m';
  102. }
  103. return '10m';
  104. }
  105. /**
  106. * Duplicate of getInterval, except that we do not support <1h granularity
  107. * Used by OrgStatsV2 API
  108. */
  109. export function getSeriesApiInterval(datetimeObj: DateTimeObject) {
  110. const diffInMinutes = getDiffInMinutes(datetimeObj);
  111. if (diffInMinutes >= SIXTY_DAYS) {
  112. // Greater than or equal to 60 days
  113. return '1d';
  114. }
  115. if (diffInMinutes >= THIRTY_DAYS) {
  116. // Greater than or equal to 30 days
  117. return '4h';
  118. }
  119. return '1h';
  120. }
  121. export function getDiffInMinutes(datetimeObj: DateTimeObject): number {
  122. const {period, start, end} = datetimeObj;
  123. if (start && end) {
  124. return moment(end).diff(start, 'minutes');
  125. }
  126. return (
  127. parsePeriodToHours(typeof period === 'string' ? period : DEFAULT_STATS_PERIOD) * 60
  128. );
  129. }
  130. // Max period (in hours) before we can no long include previous period
  131. const MAX_PERIOD_HOURS_INCLUDE_PREVIOUS = 45 * 24;
  132. export function canIncludePreviousPeriod(
  133. includePrevious: boolean | undefined,
  134. period: string | null | undefined
  135. ) {
  136. if (!includePrevious) {
  137. return false;
  138. }
  139. if (period && parsePeriodToHours(period) > MAX_PERIOD_HOURS_INCLUDE_PREVIOUS) {
  140. return false;
  141. }
  142. // otherwise true
  143. return !!includePrevious;
  144. }
  145. export function shouldFetchPreviousPeriod({
  146. includePrevious = true,
  147. period,
  148. start,
  149. end,
  150. }: {
  151. includePrevious?: boolean;
  152. } & Pick<DateTimeObject, 'start' | 'end' | 'period'>) {
  153. return !start && !end && canIncludePreviousPeriod(includePrevious, period);
  154. }
  155. /**
  156. * Generates a series selection based on the query parameters defined by the location.
  157. */
  158. export function getSeriesSelection(
  159. location: Location
  160. ): LegendComponentOption['selected'] {
  161. const unselectedSeries = decodeList(location?.query.unselectedSeries);
  162. return unselectedSeries.reduce((selection, series) => {
  163. selection[series] = false;
  164. return selection;
  165. }, {});
  166. }
  167. function isSingleSeriesStats(
  168. data: MultiSeriesEventsStats | EventsStats
  169. ): data is EventsStats {
  170. return (
  171. (defined(data.data) || defined(data.totals)) &&
  172. defined(data.start) &&
  173. defined(data.end)
  174. );
  175. }
  176. export function isMultiSeriesStats(
  177. data: MultiSeriesEventsStats | EventsStats | null | undefined,
  178. isTopN?: boolean
  179. ): data is MultiSeriesEventsStats {
  180. return (
  181. defined(data) &&
  182. ((data.data === undefined && data.totals === undefined) ||
  183. (defined(isTopN) && isTopN && defined(data) && !!!isSingleSeriesStats(data))) // the isSingleSeriesStats check is for topN queries returning null data
  184. );
  185. }
  186. // If dimension is a number convert it to pixels, otherwise use dimension
  187. // without transform
  188. export const getDimensionValue = (dimension?: number | string | null) => {
  189. if (typeof dimension === 'number') {
  190. return `${dimension}px`;
  191. }
  192. if (dimension === null) {
  193. return undefined;
  194. }
  195. return dimension;
  196. };
  197. const RGB_LIGHTEN_VALUE = 30;
  198. export const lightenHexToRgb = (colors: string[]) =>
  199. colors.map(hex => {
  200. const rgb = [
  201. Math.min(parseInt(hex.slice(1, 3), 16) + RGB_LIGHTEN_VALUE, 255),
  202. Math.min(parseInt(hex.slice(3, 5), 16) + RGB_LIGHTEN_VALUE, 255),
  203. Math.min(parseInt(hex.slice(5, 7), 16) + RGB_LIGHTEN_VALUE, 255),
  204. ];
  205. return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
  206. });
  207. const DEFAULT_GEO_DATA = {
  208. title: '',
  209. data: [],
  210. };
  211. export const processTableResults = (tableResults?: TableDataWithTitle[]) => {
  212. if (!tableResults || !tableResults.length) {
  213. return DEFAULT_GEO_DATA;
  214. }
  215. const tableResult = tableResults[0];
  216. const {data} = tableResult;
  217. if (!data || !data.length) {
  218. return DEFAULT_GEO_DATA;
  219. }
  220. const preAggregate = Object.keys(data[0]).find(column => {
  221. return column !== 'geo.country_code';
  222. });
  223. if (!preAggregate) {
  224. return DEFAULT_GEO_DATA;
  225. }
  226. return {
  227. title: tableResult.title ?? '',
  228. data: data.map(row => {
  229. return {
  230. name: row['geo.country_code'] as string,
  231. value: row[preAggregate] as number,
  232. };
  233. }),
  234. };
  235. };
  236. export const getPreviousSeriesName = (seriesName: string) => {
  237. return `previous ${seriesName}`;
  238. };