utils.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import type {Location, Query} from 'history';
  2. import pick from 'lodash/pick';
  3. import {DEFAULT_RELATIVE_PERIODS} from 'sentry/constants';
  4. import {t} from 'sentry/locale';
  5. import {defined} from 'sentry/utils';
  6. import EventView from 'sentry/utils/discover/eventView';
  7. import {isAggregateField} from 'sentry/utils/discover/fields';
  8. import type {SpanSlug} from 'sentry/utils/performance/suspectSpans/types';
  9. import {decodeScalar} from 'sentry/utils/queryString';
  10. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  11. import type {SpanSort, SpanSortOption} from './types';
  12. import {SpanSortOthers, SpanSortPercentiles} from './types';
  13. export function generateSpansRoute({orgSlug}: {orgSlug: string}): string {
  14. return `/organizations/${orgSlug}/performance/summary/spans/`;
  15. }
  16. export function spansRouteWithQuery({
  17. orgSlug,
  18. transaction,
  19. projectID,
  20. query,
  21. }: {
  22. orgSlug: string;
  23. query: Query;
  24. transaction: string;
  25. projectID?: string | string[];
  26. }) {
  27. const pathname = generateSpansRoute({
  28. orgSlug,
  29. });
  30. return {
  31. pathname,
  32. query: {
  33. transaction,
  34. project: projectID,
  35. environment: query.environment,
  36. statsPeriod: query.statsPeriod,
  37. start: query.start,
  38. end: query.end,
  39. query: query.query,
  40. },
  41. };
  42. }
  43. export const SPAN_RETENTION_DAYS = 30;
  44. export const SPAN_RELATIVE_PERIODS = pick(DEFAULT_RELATIVE_PERIODS, [
  45. '1h',
  46. '24h',
  47. '7d',
  48. '14d',
  49. '30d',
  50. ]);
  51. export const SPAN_SORT_OPTIONS: SpanSortOption[] = [
  52. {
  53. prefix: t('Sort By'),
  54. label: t('Total Self Time'),
  55. field: SpanSortOthers.SUM_EXCLUSIVE_TIME,
  56. },
  57. {
  58. prefix: t('Sort By'),
  59. label: t('Average Count'),
  60. field: SpanSortOthers.AVG_OCCURRENCE,
  61. },
  62. {
  63. prefix: t('Sort By'),
  64. label: t('p50 Self Time'),
  65. field: SpanSortPercentiles.P50_EXCLUSIVE_TIME,
  66. },
  67. {
  68. prefix: t('Sort By'),
  69. label: t('p75 Self Time'),
  70. field: SpanSortPercentiles.P75_EXCLUSIVE_TIME,
  71. },
  72. {
  73. prefix: t('Sort By'),
  74. label: t('p95 Self Time'),
  75. field: SpanSortPercentiles.P95_EXCLUSIVE_TIME,
  76. },
  77. {
  78. prefix: t('Sort By'),
  79. label: t('p99 Self Time'),
  80. field: SpanSortPercentiles.P99_EXCLUSIVE_TIME,
  81. },
  82. ];
  83. const DEFAULT_SORT = SpanSortOthers.SUM_EXCLUSIVE_TIME;
  84. function getSuspectSpanSort(sort: string): SpanSortOption {
  85. const selected = SPAN_SORT_OPTIONS.find(option => option.field === sort);
  86. if (selected) {
  87. return selected;
  88. }
  89. return SPAN_SORT_OPTIONS.find(option => option.field === DEFAULT_SORT)!;
  90. }
  91. export function getSuspectSpanSortFromLocation(
  92. location: Location,
  93. sortKey: string = 'sort'
  94. ): SpanSortOption {
  95. const sort = decodeScalar(location?.query?.[sortKey]) ?? DEFAULT_SORT;
  96. return getSuspectSpanSort(sort);
  97. }
  98. export function getSuspectSpanSortFromEventView(eventView: EventView): SpanSortOption {
  99. const sort = eventView.sorts.length ? eventView.sorts[0].field : DEFAULT_SORT;
  100. return getSuspectSpanSort(sort);
  101. }
  102. export function parseSpanSlug(spanSlug: string | undefined): SpanSlug | undefined {
  103. if (!defined(spanSlug)) {
  104. return undefined;
  105. }
  106. const delimiterPos = spanSlug.lastIndexOf(':');
  107. if (delimiterPos < 0) {
  108. return undefined;
  109. }
  110. const op = spanSlug.slice(0, delimiterPos);
  111. const group = spanSlug.slice(delimiterPos + 1);
  112. return {op, group};
  113. }
  114. export function generateSpansEventView({
  115. location,
  116. transactionName,
  117. }: {
  118. location: Location;
  119. transactionName: string;
  120. }): EventView {
  121. const query = decodeScalar(location.query.query, '');
  122. const conditions = new MutableSearch(query);
  123. conditions.setFilterValues('event.type', ['transaction']);
  124. conditions.setFilterValues('transaction', [transactionName]);
  125. Object.keys(conditions.filters).forEach(field => {
  126. if (isAggregateField(field)) {
  127. conditions.removeFilter(field);
  128. }
  129. });
  130. const eventView = EventView.fromNewQueryWithLocation(
  131. {
  132. id: undefined,
  133. version: 2,
  134. name: transactionName,
  135. fields: [
  136. ...Object.values(SpanSortOthers),
  137. ...Object.values(SpanSortPercentiles),
  138. 'trace',
  139. ],
  140. query: conditions.formatString(),
  141. projects: [],
  142. },
  143. location
  144. );
  145. const sort = getSuspectSpanSortFromLocation(location);
  146. return eventView.withSorts([{field: sort.field, kind: 'desc'}]);
  147. }
  148. /**
  149. * For the totals view, we want to get some transaction level stats like
  150. * the number of transactions and the sum of the transaction duration.
  151. * This requires the removal of any aggregate conditions as they can result
  152. * in unexpected empty responses.
  153. */
  154. export function getTotalsView(eventView: EventView): EventView {
  155. const totalsView = eventView.withColumns([
  156. {kind: 'function', function: ['count', '', undefined, undefined]},
  157. {kind: 'function', function: ['sum', 'transaction.duration', undefined, undefined]},
  158. ]);
  159. const conditions = new MutableSearch(eventView.query);
  160. // filter out any aggregate conditions
  161. Object.keys(conditions.filters).forEach(field => {
  162. if (isAggregateField(field)) {
  163. conditions.removeFilter(field);
  164. }
  165. });
  166. totalsView.query = conditions.formatString();
  167. return totalsView;
  168. }
  169. export const SPAN_SORT_TO_FIELDS: Record<SpanSort, string[]> = {
  170. [SpanSortOthers.SUM_EXCLUSIVE_TIME]: [
  171. 'percentileArray(spans_exclusive_time, 0.75)',
  172. 'count()',
  173. 'count_unique(id)',
  174. 'sumArray(spans_exclusive_time)',
  175. ],
  176. [SpanSortOthers.AVG_OCCURRENCE]: [
  177. 'percentileArray(spans_exclusive_time, 0.75)',
  178. 'count()',
  179. 'count_unique(id)',
  180. 'equation|count() / count_unique(id)',
  181. 'sumArray(spans_exclusive_time)',
  182. ],
  183. [SpanSortPercentiles.P50_EXCLUSIVE_TIME]: [
  184. 'percentileArray(spans_exclusive_time, 0.5)',
  185. 'count()',
  186. 'count_unique(id)',
  187. 'sumArray(spans_exclusive_time)',
  188. ],
  189. [SpanSortPercentiles.P75_EXCLUSIVE_TIME]: [
  190. 'percentileArray(spans_exclusive_time, 0.75)',
  191. 'count()',
  192. 'count_unique(id)',
  193. 'sumArray(spans_exclusive_time)',
  194. ],
  195. [SpanSortPercentiles.P95_EXCLUSIVE_TIME]: [
  196. 'percentileArray(spans_exclusive_time, 0.95)',
  197. 'count()',
  198. 'count_unique(id)',
  199. 'sumArray(spans_exclusive_time)',
  200. ],
  201. [SpanSortPercentiles.P99_EXCLUSIVE_TIME]: [
  202. 'percentileArray(spans_exclusive_time, 0.99)',
  203. 'count()',
  204. 'count_unique(id)',
  205. 'sumArray(spans_exclusive_time)',
  206. ],
  207. };
  208. export function getExclusiveTimeDisplayedValue(value: string): string {
  209. return value.replace('exclusive', 'self');
  210. }