metrics.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import omit from 'lodash/omit';
  2. import {doMetricsRequest} from 'sentry/actionCreators/metrics';
  3. import {Client, ResponseMeta} from 'sentry/api';
  4. import {t} from 'sentry/locale';
  5. import {
  6. MetricsApiResponse,
  7. Organization,
  8. PageFilters,
  9. SessionApiResponse,
  10. SessionField,
  11. } from 'sentry/types';
  12. import {Series} from 'sentry/types/echarts';
  13. import {TableData} from 'sentry/utils/discover/discoverQuery';
  14. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  15. import {OnDemandControlContext} from 'sentry/utils/performance/contexts/onDemandControl';
  16. import {ReleaseSearchBar} from 'sentry/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/releaseSearchBar';
  17. import {DisplayType, Widget, WidgetQuery} from '../types';
  18. import {getWidgetInterval} from '../utils';
  19. import {resolveDerivedStatusFields} from '../widgetCard/metricWidgetQueries';
  20. import {getSeriesName} from '../widgetCard/transformSessionsResponseToSeries';
  21. import {
  22. changeObjectValuesToTypes,
  23. getDerivedMetrics,
  24. mapDerivedMetricsToFields,
  25. } from '../widgetCard/transformSessionsResponseToTable';
  26. import {DatasetConfig, handleOrderByReset} from './base';
  27. const DEFAULT_WIDGET_QUERY: WidgetQuery = {
  28. name: '',
  29. fields: [`avg(duration)`],
  30. columns: [],
  31. fieldAliases: [],
  32. aggregates: [`avg(duration)`],
  33. conditions: '',
  34. orderby: `-avg(duration)`,
  35. };
  36. export const MetricsConfig: DatasetConfig<MetricsApiResponse, MetricsApiResponse> = {
  37. defaultWidgetQuery: DEFAULT_WIDGET_QUERY,
  38. enableEquations: false,
  39. getTableRequest: (
  40. api: Client,
  41. _: Widget,
  42. query: WidgetQuery,
  43. organization: Organization,
  44. pageFilters: PageFilters,
  45. __?: OnDemandControlContext,
  46. limit?: number,
  47. cursor?: string
  48. ) =>
  49. getMetricRequest(
  50. 0,
  51. 1,
  52. api,
  53. query,
  54. organization,
  55. pageFilters,
  56. undefined,
  57. limit,
  58. cursor
  59. ),
  60. getSeriesRequest: getMetricSeriesRequest,
  61. getCustomFieldRenderer: (field, meta) => getFieldRenderer(field, meta, false),
  62. // TODO(ddm): check if we need a MetricSearchBar
  63. SearchBar: ReleaseSearchBar,
  64. handleColumnFieldChangeOverride,
  65. handleOrderByReset: handleMetricTableOrderByReset,
  66. supportedDisplayTypes: [
  67. DisplayType.AREA,
  68. DisplayType.BAR,
  69. DisplayType.BIG_NUMBER,
  70. DisplayType.LINE,
  71. DisplayType.TABLE,
  72. DisplayType.TOP_N,
  73. ],
  74. transformSeries: transformSessionsResponseToSeries,
  75. transformTable: transformSessionsResponseToTable,
  76. getTableFieldOptions: () => ({}),
  77. getTableSortOptions: undefined,
  78. };
  79. function getMetricSeriesRequest(
  80. api: Client,
  81. widget: Widget,
  82. queryIndex: number,
  83. organization: Organization,
  84. pageFilters: PageFilters
  85. ) {
  86. const query = widget.queries[queryIndex];
  87. const {displayType, limit} = widget;
  88. const {datetime} = pageFilters;
  89. const {start, end, period} = datetime;
  90. const includeTotals = query.columns.length > 0 ? 1 : 0;
  91. const interval = getWidgetInterval(
  92. displayType,
  93. {start, end, period},
  94. '5m'
  95. // requesting low fidelity for release sort because metrics api can't return 100 rows of high fidelity series data
  96. );
  97. return getMetricRequest(
  98. 1,
  99. includeTotals,
  100. api,
  101. query,
  102. organization,
  103. pageFilters,
  104. interval,
  105. limit
  106. );
  107. }
  108. function handleMetricTableOrderByReset(widgetQuery: WidgetQuery, newFields: string[]) {
  109. const disableSortBy = widgetQuery.columns.includes('session.status');
  110. if (disableSortBy) {
  111. widgetQuery.orderby = '';
  112. }
  113. return handleOrderByReset(widgetQuery, newFields);
  114. }
  115. function handleColumnFieldChangeOverride(widgetQuery: WidgetQuery): WidgetQuery {
  116. if (widgetQuery.aggregates.length === 0) {
  117. // Release Health widgets require an aggregate in tables
  118. const defaultReleaseHealthAggregate = `crash_free_rate(${SessionField.SESSION})`;
  119. widgetQuery.aggregates = [defaultReleaseHealthAggregate];
  120. widgetQuery.fields = widgetQuery.fields
  121. ? [...widgetQuery.fields, defaultReleaseHealthAggregate]
  122. : [defaultReleaseHealthAggregate];
  123. }
  124. return widgetQuery;
  125. }
  126. export function transformSessionsResponseToTable(
  127. data: SessionApiResponse | MetricsApiResponse,
  128. widgetQuery: WidgetQuery
  129. ): TableData {
  130. const {derivedStatusFields, injectedFields} = resolveDerivedStatusFields(
  131. widgetQuery.aggregates
  132. );
  133. const rows = data.groups.map((group, index) => ({
  134. id: String(index),
  135. ...mapDerivedMetricsToFields(group.by),
  136. // if `sum(session)` or `count_unique(user)` are not
  137. // requested as a part of the payload for
  138. // derived status metrics through the Sessions API,
  139. // they are injected into the payload and need to be
  140. // stripped.
  141. ...omit(mapDerivedMetricsToFields(group.totals), injectedFields),
  142. // if session.status is a groupby, some post processing
  143. // is needed to calculate the status derived metrics
  144. // from grouped results of `sum(session)` or `count_unique(user)`
  145. ...getDerivedMetrics(group.by, group.totals, derivedStatusFields),
  146. }));
  147. const singleRow = rows[0];
  148. const meta = {
  149. ...changeObjectValuesToTypes(omit(singleRow, 'id')),
  150. };
  151. return {meta, data: rows};
  152. }
  153. export function transformSessionsResponseToSeries(
  154. data: SessionApiResponse | MetricsApiResponse,
  155. widgetQuery: WidgetQuery
  156. ) {
  157. if (data === null) {
  158. return [];
  159. }
  160. const queryAlias = widgetQuery.name;
  161. const results: Series[] = [];
  162. if (!data.groups.length) {
  163. return [
  164. {
  165. seriesName: `(${t('no results')})`,
  166. data: data.intervals.map(interval => ({
  167. name: interval,
  168. value: 0,
  169. })),
  170. },
  171. ];
  172. }
  173. data.groups.forEach(group => {
  174. Object.keys(group.series).forEach(field => {
  175. results.push({
  176. seriesName: getSeriesName(field, group, queryAlias),
  177. data: data.intervals.map((interval, index) => ({
  178. name: interval,
  179. value: group.series[field][index] ?? 0,
  180. })),
  181. });
  182. });
  183. });
  184. return results;
  185. }
  186. function getMetricRequest(
  187. includeSeries: number,
  188. includeTotals: number,
  189. api: Client,
  190. query: WidgetQuery,
  191. organization: Organization,
  192. pageFilters: PageFilters,
  193. interval?: string,
  194. limit?: number,
  195. cursor?: string
  196. ) {
  197. const {environments, projects, datetime} = pageFilters;
  198. const {start, end, period} = datetime;
  199. const columns = query.columns;
  200. const requestData = {
  201. field: query.aggregates,
  202. orgSlug: organization.slug,
  203. end,
  204. environment: environments,
  205. groupBy: columns,
  206. limit: columns.length === 0 ? 1 : limit,
  207. orderBy: '',
  208. interval,
  209. project: projects,
  210. query: query.conditions,
  211. start,
  212. statsPeriod: period,
  213. includeAllArgs: true,
  214. cursor,
  215. includeSeries,
  216. includeTotals,
  217. };
  218. // TODO(ddm): get rid of this cast
  219. return doMetricsRequest(api, requestData) as Promise<
  220. [MetricsApiResponse, string | undefined, ResponseMeta | undefined]
  221. >;
  222. }