123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- import type {Series} from 'sentry/types/echarts';
- import type {
- EventsStats,
- GroupedMultiSeriesEventsStats,
- MultiSeriesEventsStats,
- } from 'sentry/types/organization';
- import {encodeSort} from 'sentry/utils/discover/eventView';
- import {DURATION_UNITS, SIZE_UNITS} from 'sentry/utils/discover/fieldRenderers';
- import {getAggregateAlias} from 'sentry/utils/discover/fields';
- import {
- type DiscoverQueryProps,
- useGenericDiscoverQuery,
- } from 'sentry/utils/discover/genericDiscoverQuery';
- import {DiscoverDatasets} from 'sentry/utils/discover/types';
- import type {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import {useLocation} from 'sentry/utils/useLocation';
- import useOrganization from 'sentry/utils/useOrganization';
- import usePageFilters from 'sentry/utils/usePageFilters';
- import {getSeriesEventView} from 'sentry/views/insights/common/queries/getSeriesEventView';
- import type {SpanFunctions, SpanIndexedField} from 'sentry/views/insights/types';
- import {getRetryDelay, shouldRetryHandler} from '../utils/retryHandlers';
- type SeriesMap = {
- [seriesName: string]: Series[];
- };
- interface Options<Fields> {
- enabled?: boolean;
- fields?: string[];
- interval?: string;
- orderby?: string | string[];
- overriddenRoute?: string;
- referrer?: string;
- search?: MutableSearch;
- topEvents?: number;
- yAxis?: Fields;
- }
- export const useSortedTimeSeries = <
- Fields extends SpanIndexedField[] | SpanFunctions[] | string[],
- >(
- options: Options<Fields> = {},
- referrer: string,
- dataset?: DiscoverDatasets
- ) => {
- const location = useLocation();
- const organization = useOrganization();
- const {
- search,
- yAxis = [],
- interval,
- topEvents,
- fields,
- orderby,
- overriddenRoute,
- enabled,
- } = options;
- const pageFilters = usePageFilters();
- const eventView = getSeriesEventView(
- search,
- fields,
- pageFilters.selection,
- yAxis,
- topEvents,
- dataset ?? DiscoverDatasets.SPANS_INDEXED,
- orderby
- );
- if (interval) {
- eventView.interval = interval;
- }
- const result = useGenericDiscoverQuery<
- MultiSeriesEventsStats | GroupedMultiSeriesEventsStats,
- DiscoverQueryProps
- >({
- route: overriddenRoute ?? 'events-stats',
- eventView,
- location,
- orgSlug: organization.slug,
- getRequestPayload: () => ({
- ...eventView.getEventsAPIPayload(location),
- yAxis: eventView.yAxis,
- topEvents: eventView.topEvents,
- excludeOther: 0,
- partial: 1,
- orderby: eventView.sorts?.[0] ? encodeSort(eventView.sorts?.[0]) : undefined,
- interval: eventView.interval,
- }),
- options: {
- enabled: enabled && pageFilters.isReady,
- refetchOnWindowFocus: false,
- retry: shouldRetryHandler,
- retryDelay: getRetryDelay,
- staleTime: Infinity,
- },
- referrer,
- });
- const isFetchingOrLoading = result.isPending || result.isFetching;
- const data: SeriesMap = isFetchingOrLoading
- ? {}
- : transformToSeriesMap(result.data, yAxis);
- const pageLinks = result.response?.getResponseHeader('Link') ?? undefined;
- return {
- ...result,
- pageLinks,
- data,
- meta: result.data?.meta,
- };
- };
- export function isEventsStats(
- obj: EventsStats | MultiSeriesEventsStats | GroupedMultiSeriesEventsStats
- ): obj is EventsStats {
- return typeof obj === 'object' && obj !== null && typeof obj.data === 'object';
- }
- function isMultiSeriesEventsStats(
- obj: EventsStats | MultiSeriesEventsStats | GroupedMultiSeriesEventsStats
- ): obj is MultiSeriesEventsStats {
- if (typeof obj !== 'object' || obj === null) {
- return false;
- }
- return Object.values(obj).every(series => isEventsStats(series));
- }
- function transformToSeriesMap(
- result: MultiSeriesEventsStats | GroupedMultiSeriesEventsStats | undefined,
- yAxis: string[]
- ): SeriesMap {
- if (!result) {
- return {};
- }
- // Single series, applies to single axis queries
- const firstYAxis = yAxis[0] || '';
- if (isEventsStats(result)) {
- const [, series] = processSingleEventStats(firstYAxis, result);
- return {
- [firstYAxis]: [series],
- };
- }
- // Multiple series, applies to multi axis or topN events queries
- const hasMultipleYAxes = yAxis.length > 1;
- if (isMultiSeriesEventsStats(result)) {
- const processedResults: [number, Series][] = Object.keys(result).map(seriesName =>
- processSingleEventStats(seriesName, result[seriesName])
- );
- if (!hasMultipleYAxes) {
- return {
- [firstYAxis]: processedResults
- .sort(([a], [b]) => a - b)
- .map(([, series]) => series),
- };
- }
- return processedResults
- .sort(([a], [b]) => a - b)
- .reduce((acc, [, series]) => {
- acc[series.seriesName] = [series];
- return acc;
- }, {});
- }
- // Grouped multi series, applies to topN events queries with multiple y-axes
- // First, we process the grouped multi series into a list of [seriesName, order, {[aggFunctionAlias]: EventsStats}]
- // to enable sorting.
- const processedResults: [string, number, MultiSeriesEventsStats][] = [];
- Object.keys(result).forEach(seriesName => {
- const {order: groupOrder, ...groupData} = result[seriesName];
- processedResults.push([seriesName, groupOrder || 0, groupData]);
- });
- return processedResults
- .sort(([, orderA], [, orderB]) => orderA - orderB)
- .reduce((acc, [seriesName, , groupData]) => {
- Object.keys(groupData).forEach(aggFunctionAlias => {
- const [, series] = processSingleEventStats(
- seriesName,
- groupData[aggFunctionAlias]
- );
- if (!acc[aggFunctionAlias]) {
- acc[aggFunctionAlias] = [series];
- } else {
- acc[aggFunctionAlias].push(series);
- }
- });
- return acc;
- }, {} as SeriesMap);
- }
- function processSingleEventStats(
- seriesName: string,
- seriesData: EventsStats
- ): [number, Series] {
- let scale = 1;
- if (seriesName) {
- const unit = seriesData.meta?.units?.[getAggregateAlias(seriesName)];
- // Scale series values to milliseconds or bytes depending on units from meta
- scale = (unit && (DURATION_UNITS[unit] ?? SIZE_UNITS[unit])) ?? 1;
- }
- const processsedData: Series = {
- seriesName: seriesName || '(empty string)',
- data: seriesData.data.map(([timestamp, countsForTimestamp]) => ({
- name: timestamp * 1000,
- value: countsForTimestamp.reduce((acc, {count}) => acc + count, 0) * scale,
- })),
- };
- return [seriesData.order || 0, processsedData];
- }
|