diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.spec.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx similarity index 87% rename from static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.spec.tsx rename to static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx index f42ed3aa01b38f..18e9c773909a78 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.spec.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx @@ -1,6 +1,6 @@ import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; -import {formatTimeSeriesName} from './formatTimeSeriesName'; +import {formatTimeSeriesLabel} from './formatTimeSeriesLabel'; describe('formatSeriesName', () => { describe('releases', () => { @@ -12,7 +12,7 @@ describe('formatSeriesName', () => { yAxis: name, }); - expect(formatTimeSeriesName(timeSeries)).toEqual(result); + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); }); }); @@ -26,7 +26,7 @@ describe('formatSeriesName', () => { yAxis: name, }); - expect(formatTimeSeriesName(timeSeries)).toEqual(result); + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); }); }); @@ -40,7 +40,7 @@ describe('formatSeriesName', () => { yAxis: name, }); - expect(formatTimeSeriesName(timeSeries)).toEqual(result); + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); }); }); @@ -53,7 +53,7 @@ describe('formatSeriesName', () => { yAxis: name, }); - expect(formatTimeSeriesName(timeSeries)).toEqual(result); + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); }); }); @@ -66,7 +66,7 @@ describe('formatSeriesName', () => { yAxis: name, }); - expect(formatTimeSeriesName(timeSeries)).toEqual(result); + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); }); }); @@ -79,7 +79,7 @@ describe('formatSeriesName', () => { yAxis: name, }); - expect(formatTimeSeriesName(timeSeries)).toEqual(result); + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); }); }); @@ -145,7 +145,7 @@ describe('formatSeriesName', () => { groupBy, }); - expect(formatTimeSeriesName(timeSeries)).toEqual(result); + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); }); }); @@ -157,7 +157,7 @@ describe('formatSeriesName', () => { isOther: true, }; - expect(formatTimeSeriesName(timeSeries)).toBe('Other'); + expect(formatTimeSeriesLabel(timeSeries)).toBe('Other'); }); }); }); diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx new file mode 100644 index 00000000000000..9037f6893a6ce5 --- /dev/null +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx @@ -0,0 +1,70 @@ +import {t} from 'sentry/locale'; +import { + getAggregateArg, + getMeasurementSlug, + maybeEquationAlias, + stripEquationPrefix, +} from 'sentry/utils/discover/fields'; +import {formatVersion} from 'sentry/utils/versions/formatVersion'; +import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder'; +import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types'; + +export function formatTimeSeriesLabel(timeSeries: TimeSeries): string { + // If the timeSeries has `groupBy` information, the label is made by + // concatenating the values of the groupBy, since there's no point repeating + // the name of the Y axis multiple times in the legend. + if (timeSeries.meta.isOther) { + return t('Other'); + } + + if (timeSeries.groupBy?.length && timeSeries.groupBy.length > 0) { + return `${timeSeries.groupBy + ?.map(groupBy => { + if (Array.isArray(groupBy.value)) { + return JSON.stringify(groupBy.value); + } + + if (groupBy.key === 'release') { + return formatVersion(groupBy.value); + } + + return groupBy.value; + }) + .join(',')}`; + } + + let {yAxis: seriesName} = timeSeries; + + // Decode from series name disambiguation + seriesName = WidgetLegendNameEncoderDecoder.decodeSeriesNameForLegend(seriesName)!; + + // Attempt to parse the `seriesName` as a version. A correct `TimeSeries` + // would have a `yAxis` like `p50(span.duration)` with a `groupBy` like + // `[{key: "release", value: "proj@1.2.3"}]`. `groupBy` was only introduced + // recently though, so many `TimeSeries` instead mash the group by information + // into the `yAxis` property, e.g., the `yAxis` might have been set to + // `"proj@1.2.3"` just to get the correct rendering in the chart legend. We + // cover these cases by parsing the `yAxis` as a series name. This works badly + // because sometimes it'll interpet email addresses as versions, which causes + // bugs. We should update all usages of `TimeSeriesWidgetVisualization` to + // correctly specify `yAxis` and `groupBy`, and/or to use the time + // `/events-timeseries` endpoint which does this automatically. + seriesName = formatVersion(seriesName); + + // Check for special-case measurement formatting + const arg = getAggregateArg(seriesName); + if (arg) { + const slug = getMeasurementSlug(arg); + + if (slug) { + seriesName = slug.toUpperCase(); + } + } + + // Strip equation prefix + if (maybeEquationAlias(seriesName)) { + seriesName = stripEquationPrefix(seriesName); + } + + return seriesName; +} diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.tsx index dd410ec9617456..879a04de8a0bc8 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.tsx @@ -1,70 +1,15 @@ -import {t} from 'sentry/locale'; -import { - getAggregateArg, - getMeasurementSlug, - maybeEquationAlias, - stripEquationPrefix, -} from 'sentry/utils/discover/fields'; -import {formatVersion} from 'sentry/utils/versions/formatVersion'; -import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder'; import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types'; export function formatTimeSeriesName(timeSeries: TimeSeries): string { - // If the timeSeries has `groupBy` information, the label is made by - // concatenating the values of the groupBy, since there's no point repeating - // the name of the Y axis multiple times in the legend. - if (timeSeries.meta.isOther) { - return t('Other'); - } + let name = `${timeSeries.yAxis}`; - if (timeSeries.groupBy?.length && timeSeries.groupBy.length > 0) { - return `${timeSeries.groupBy + if (timeSeries.groupBy?.length) { + name += ` : ${timeSeries.groupBy ?.map(groupBy => { - if (Array.isArray(groupBy.value)) { - return JSON.stringify(groupBy.value); - } - - if (groupBy.key === 'release') { - return formatVersion(groupBy.value); - } - - return groupBy.value; + return `${groupBy.key} : ${groupBy.value}`; }) .join(',')}`; } - let {yAxis: seriesName} = timeSeries; - - // Decode from series name disambiguation - seriesName = WidgetLegendNameEncoderDecoder.decodeSeriesNameForLegend(seriesName)!; - - // Attempt to parse the `seriesName` as a version. A correct `TimeSeries` - // would have a `yAxis` like `p50(span.duration)` with a `groupBy` like - // `[{key: "release", value: "proj@1.2.3"}]`. `groupBy` was only introduced - // recently though, so many `TimeSeries` instead mash the group by information - // into the `yAxis` property, e.g., the `yAxis` might have been set to - // `"proj@1.2.3"` just to get the correct rendering in the chart legend. We - // cover these cases by parsing the `yAxis` as a series name. This works badly - // because sometimes it'll interpet email addresses as versions, which causes - // bugs. We should update all usages of `TimeSeriesWidgetVisualization` to - // correctly specify `yAxis` and `groupBy`, and/or to use the time - // `/events-timeseries` endpoint which does this automatically. - seriesName = formatVersion(seriesName); - - // Check for special-case measurement formatting - const arg = getAggregateArg(seriesName); - if (arg) { - const slug = getMeasurementSlug(arg); - - if (slug) { - seriesName = slug.toUpperCase(); - } - } - - // Strip equation prefix - if (maybeEquationAlias(seriesName)) { - seriesName = stripEquationPrefix(seriesName); - } - - return seriesName; + return name; } diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/continuousTimeSeries.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/continuousTimeSeries.tsx index a9385c3965ffa9..02fc8cf77107f7 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/continuousTimeSeries.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/continuousTimeSeries.tsx @@ -6,6 +6,7 @@ import type { TimeSeries, TimeSeriesValueUnit, } from 'sentry/views/dashboards/widgets/common/types'; +import {formatTimeSeriesLabel} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel'; import {formatTimeSeriesName} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName'; import {FALLBACK_TYPE} from 'sentry/views/dashboards/widgets/timeSeriesWidget/settings'; @@ -62,21 +63,11 @@ export abstract class ContinuousTimeSeries< * Continuous time series names need to be unique to disambiguate them from other series. We use both the `yAxis` and the `groupBy` to create the name. This makes it possible to pass in two different time series with the same `yAxis` as long as they have different `groupBy` information. */ get name(): string { - let name = `${this.timeSeries.yAxis}`; - - if (this.timeSeries.groupBy?.length) { - name += ` : ${this.timeSeries.groupBy - ?.map(groupBy => { - return `${groupBy.key} : ${groupBy.value}`; - }) - .join(',')}`; - } - - return name; + return formatTimeSeriesName(this.timeSeries); } get label(): string { - return this.config?.alias ?? formatTimeSeriesName(this.timeSeries); + return this.config?.alias ?? formatTimeSeriesLabel(this.timeSeries); } get isEmpty(): boolean { diff --git a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx index 33221779515374..d22c68df7ab28e 100644 --- a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx +++ b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx @@ -15,6 +15,7 @@ import type { LegendSelection, TimeSeries, } from 'sentry/views/dashboards/widgets/common/types'; +import {formatTimeSeriesName} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName'; import {Area} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/area'; import {Bars} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/bars'; import {Line} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/line'; @@ -134,10 +135,16 @@ export function InsightsTimeSeriesWidget(props: InsightsTimeSeriesWidgetProps) { yAxes.add(timeSeries.yAxis); + let alias = aliases?.[delayedTimeSeries.yAxis]; + const plottableName = formatTimeSeriesName(delayedTimeSeries); + if (aliases?.[plottableName]) { + alias = aliases?.[plottableName]; + } + return new PlottableDataConstructor(delayedTimeSeries, { - color: COMMON_COLORS(theme)[delayedTimeSeries.yAxis], + color: COMMON_COLORS(theme)[plottableName], stack: props.stacked && props.visualizationType === 'bar' ? 'all' : undefined, - alias: aliases?.[delayedTimeSeries.yAxis], + alias, }); }), ...(props.extraPlottables ?? []), @@ -288,5 +295,7 @@ const COMMON_COLORS = (theme: Theme): Record => { 'performance_score(measurements.score.inp)': vitalColors[2], 'performance_score(measurements.score.cls)': vitalColors[3], 'performance_score(measurements.score.ttfb)': vitalColors[4], + 'epm() : span.op : queue.publish': colors[1], + 'epm() : span.op : queue.process': colors[2], }; }; diff --git a/static/app/views/insights/common/utils/renameDiscoverSeries.tsx b/static/app/views/insights/common/utils/renameDiscoverSeries.tsx deleted file mode 100644 index 867c9576388a04..00000000000000 --- a/static/app/views/insights/common/utils/renameDiscoverSeries.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type {DiscoverSeries} from 'sentry/views/insights/common/queries/types'; - -export function renameDiscoverSeries( - series: DiscoverSeries, - newName: string -): DiscoverSeries { - const previousName = series.seriesName; - - return { - ...series, - seriesName: newName, - meta: { - ...series.meta, - fields: { - ...series.meta?.fields, - [newName]: series.meta?.fields?.[previousName] ?? 'number', - }, - units: { - ...series.meta?.units, - [newName]: series.meta?.units?.[previousName] ?? '', - }, - }, - }; -} diff --git a/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx b/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx index 053b483272b9d8..b39f06fba7e204 100644 --- a/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx +++ b/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx @@ -3,6 +3,7 @@ import {type MutableSearch} from 'sentry/utils/tokenizeSearch'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {Dataset} from 'sentry/views/alerts/rules/metric/types'; +import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types'; import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; import {getExploreUrl} from 'sentry/views/explore/utils'; import {ChartType} from 'sentry/views/insights/common/components/chart'; @@ -10,7 +11,6 @@ import {BaseChartActionDropdown} from 'sentry/views/insights/common/components/c // TODO(release-drawer): Only used in httpSamplesPanel, should be easy to move data fetching in here // eslint-disable-next-line no-restricted-imports import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget'; -import type {DiscoverSeries} from 'sentry/views/insights/common/queries/types'; import {getAlertsUrl} from 'sentry/views/insights/common/utils/getAlertsUrl'; import type {AddToSpanDashboardOptions} from 'sentry/views/insights/common/utils/useAddToSpanDashboard'; import {useAlertsProject} from 'sentry/views/insights/common/utils/useAlertsProject'; @@ -21,7 +21,7 @@ interface Props { isLoading: boolean; referrer: string; search: MutableSearch; - series: DiscoverSeries[]; + series: TimeSeries[]; error?: Error | null; } @@ -40,38 +40,11 @@ export function ResponseCodeCountChart({ const yAxis = 'count()'; const title = t('Top 5 Response Codes'); - // TODO: Temporary hack. `DiscoverSeries` meta field and the series name don't - // match. This is annoying to work around, and will become irrelevant soon - // enough. For now, just specify the correct meta for these series since - // they're known and simple - const fieldAliases: Record = {}; - const seriesWithMeta: DiscoverSeries[] = series.map(discoverSeries => { - const newSeriesName = `${yAxis} ${discoverSeries.seriesName}`; - - fieldAliases[newSeriesName] = discoverSeries.seriesName; - - const transformedSeries: DiscoverSeries = { - ...discoverSeries, - seriesName: newSeriesName, - meta: { - fields: { - [newSeriesName]: 'integer', - }, - units: {}, - }, - }; - - return transformedSeries; - }); - - // TODO: kinda ugly, the series names have the format `count() 200` for 200 reponse codes - const topResponseCodes = seriesWithMeta - .map(s => s.seriesName.replace('count()', '').trim()) - .filter(isNumeric); + const topResponseCodes = series.map(getResponseCode).filter(isNumeric); const stringifiedSearch = search.formatString(); const queries = topResponseCodes.map(code => ({ - label: code, + label: `${code}`, query: `${stringifiedSearch} ${SpanFields.SPAN_STATUS_CODE}:${code}`, })); @@ -133,14 +106,37 @@ export function ResponseCodeCountChart({ ); } -function isNumeric(maybeNumber: string) { +function getResponseCode(series: TimeSeries) { + if (!series.groupBy) { + return undefined; + } + + const responseCodeGroupBy = series.groupBy.find( + g => g.key === SpanFields.SPAN_STATUS_CODE + ); + if (!responseCodeGroupBy) { + return undefined; + } + // This should never come back as an array, this is just to keep typescript happy + if (Array.isArray(responseCodeGroupBy.value)) { + return responseCodeGroupBy.value[0]; + } + return responseCodeGroupBy.value; +} + +function isNumeric(maybeNumber: string | number | null | undefined) { + if (!maybeNumber) { + return false; + } + if (typeof maybeNumber === 'number') { + return true; + } return /^\d+$/.test(maybeNumber); } diff --git a/static/app/views/insights/http/components/httpSamplesPanel.spec.tsx b/static/app/views/insights/http/components/httpSamplesPanel.spec.tsx index a8e330f0bb9432..72d7a2376cfd50 100644 --- a/static/app/views/insights/http/components/httpSamplesPanel.spec.tsx +++ b/static/app/views/insights/http/components/httpSamplesPanel.spec.tsx @@ -14,6 +14,7 @@ import {useLocation} from 'sentry/utils/useLocation'; import usePageFilters from 'sentry/utils/usePageFilters'; import {SAMPLING_MODE} from 'sentry/views/explore/hooks/useProgressiveQuery'; import {HTTPSamplesPanel} from 'sentry/views/insights/http/components/httpSamplesPanel'; +import {SpanFields} from 'sentry/views/insights/types'; jest.mock('sentry/utils/useLocation'); jest.mock('sentry/utils/usePageFilters'); @@ -133,7 +134,7 @@ describe('HTTPSamplesPanel', () => { }); eventsStatsRequestMock = MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/events-stats/`, + url: `/organizations/${organization.slug}/events-timeseries/`, method: 'GET', match: [ MockApiClient.matchQuery({ @@ -141,34 +142,24 @@ describe('HTTPSamplesPanel', () => { }), ], body: { - '301': { - data: [ - [1699907700, [{count: 7810.2}]], - [1699908000, [{count: 1216.8}]], - ], - meta: { - fields: { - count: 'integer', - }, - units: { - count: null, - }, - }, - }, - '304': { - data: [ - [1699907700, [{count: 2701.5}]], - [1699908000, [{count: 78.12}]], - ], - meta: { - fields: { - count: 'integer', - }, - units: { - count: null, - }, - }, - }, + timeSeries: [ + TimeSeriesFixture({ + yAxis: `epm()`, + groupBy: [{key: SpanFields.SPAN_STATUS_CODE, value: '301'}], + values: [ + {timestamp: 1699907700000, value: 7810.2}, + {timestamp: 1699908000000, value: 1216.8}, + ], + }), + TimeSeriesFixture({ + yAxis: `epm()`, + groupBy: [{key: SpanFields.SPAN_STATUS_CODE, value: '304'}], + values: [ + {timestamp: 1699907700000, value: 2701.5}, + {timestamp: 1699908000000, value: 78.12}, + ], + }), + ], }, }); @@ -230,7 +221,7 @@ describe('HTTPSamplesPanel', () => { expect(eventsStatsRequestMock).toHaveBeenNthCalledWith( 1, - `/organizations/${organization.slug}/events-stats/`, + `/organizations/${organization.slug}/events-timeseries/`, expect.objectContaining({ method: 'GET', query: { @@ -238,19 +229,17 @@ describe('HTTPSamplesPanel', () => { sampling: SAMPLING_MODE.NORMAL, environment: [], excludeOther: 0, - field: ['span.status_code', 'count()'], + groupBy: [SpanFields.SPAN_STATUS_CODE], interval: '30m', - orderby: '-count()', + sort: '-count()', partial: 1, - per_page: 50, project: [], query: 'span.op:http.client !has:span.domain transaction:/api/0/users span.status_code:[300,301,302,303,304,305,307,308]', referrer: 'api.insights.http.samples-panel-response-code-chart', statsPeriod: '10d', - topEvents: '5', - transformAliasToInputFormat: '0', - yAxis: 'count()', + topEvents: 5, + yAxis: ['count()'], }, }) ); diff --git a/static/app/views/insights/http/components/httpSamplesPanel.tsx b/static/app/views/insights/http/components/httpSamplesPanel.tsx index 102138f4f50360..67dc7fd100eba4 100644 --- a/static/app/views/insights/http/components/httpSamplesPanel.tsx +++ b/static/app/views/insights/http/components/httpSamplesPanel.tsx @@ -37,7 +37,6 @@ import {ReadoutRibbon} from 'sentry/views/insights/common/components/ribbon'; import {SampleDrawerBody} from 'sentry/views/insights/common/components/sampleDrawerBody'; import {SampleDrawerHeaderTransaction} from 'sentry/views/insights/common/components/sampleDrawerHeaderTransaction'; import {useSpans} from 'sentry/views/insights/common/queries/useDiscover'; -import {useTopNSpanSeries} from 'sentry/views/insights/common/queries/useTopNDiscoverSeries'; import { DataTitles, getDurationChartTitle, @@ -204,12 +203,12 @@ export function HTTPSamplesPanel() { isFetching: isResponseCodeDataLoading, data: responseCodeData, error: responseCodeError, - } = useTopNSpanSeries( + } = useFetchSpanTimeSeries( { - search, - fields: [SpanFields.SPAN_STATUS_CODE, 'count()'], + query: search, + groupBy: [SpanFields.SPAN_STATUS_CODE], yAxis: ['count()'], - topN: 5, + topEvents: 5, sort: { kind: 'desc', field: 'count()', @@ -219,6 +218,8 @@ export function HTTPSamplesPanel() { Referrer.SAMPLES_PANEL_RESPONSE_CODE_CHART ); + const responseCodeTimeSeries = responseCodeData?.timeSeries || []; + const durationAxisMax = computeAxisMax([ durationSeries ? { @@ -446,7 +447,7 @@ export function HTTPSamplesPanel() { search={search} referrer={Referrer.SAMPLES_PANEL_RESPONSE_CODE_CHART} groupBy={[SpanFields.SPAN_STATUS_CODE]} - series={Object.values(responseCodeData).filter(Boolean)} + series={responseCodeTimeSeries} isLoading={isResponseCodeDataLoading} error={responseCodeError} /> diff --git a/static/app/views/insights/mobile/appStarts/components/startDurationWidget.tsx b/static/app/views/insights/mobile/appStarts/components/startDurationWidget.tsx index 7050bc9368f572..b4cc13208676c3 100644 --- a/static/app/views/insights/mobile/appStarts/components/startDurationWidget.tsx +++ b/static/app/views/insights/mobile/appStarts/components/startDurationWidget.tsx @@ -1,12 +1,12 @@ import {t} from 'sentry/locale'; import {decodeScalar} from 'sentry/utils/queryString'; +import {useFetchSpanTimeSeries} from 'sentry/utils/timeSeries/useFetchEventsTimeSeries'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocation} from 'sentry/utils/useLocation'; // TODO(release-drawer): Only used in mobile/appStarts/components/ // eslint-disable-next-line no-restricted-imports import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget'; import {useReleaseSelection} from 'sentry/views/insights/common/queries/useReleases'; -import {useTopNSpanSeries} from 'sentry/views/insights/common/queries/useTopNDiscoverSeries'; import {appendReleaseFilters} from 'sentry/views/insights/common/utils/releaseComparison'; import {COLD_START_TYPE} from 'sentry/views/insights/mobile/appStarts/components/startTypeSelector'; import {Referrer} from 'sentry/views/insights/mobile/appStarts/referrers'; @@ -58,31 +58,30 @@ function StartDurationWidget({additionalFilters}: Props) { data, isPending: isSeriesLoading, error: seriesError, - } = useTopNSpanSeries( + } = useFetchSpanTimeSeries( { yAxis: [yAxis], - fields: [groupBy, 'avg(span.duration)'], - topN: 2, - search, + groupBy: [groupBy], + topEvents: 2, + query: search, enabled: !isReleasesLoading, }, referrer ); + const timeSeries = data?.timeSeries || []; + // Only transform the data is we know there's at least one release - const sortedSeries = data - .sort((releaseA, _releaseB) => (releaseA.seriesName === primaryRelease ? -1 : 1)) - .map(serie => ({ - ...serie, - seriesName: `${yAxis} ${serie.seriesName}`, - })); + const sortedSeries = timeSeries.sort((releaseA, _releaseB) => + releaseA.groupBy?.[0]?.value === primaryRelease ? -1 : 1 + ); return ( { ], }); MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/events-stats/`, + url: `/organizations/${organization.slug}/events-timeseries/`, + body: { + timeSeries: [ + TimeSeriesFixture({ + yAxis: 'epm()', + }), + ], + }, }); eventsMock = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/`, diff --git a/static/app/views/insights/mobile/screens/views/screenDetailsPage.spec.tsx b/static/app/views/insights/mobile/screens/views/screenDetailsPage.spec.tsx index d0c2ea9885613b..f1b0955258075f 100644 --- a/static/app/views/insights/mobile/screens/views/screenDetailsPage.spec.tsx +++ b/static/app/views/insights/mobile/screens/views/screenDetailsPage.spec.tsx @@ -2,6 +2,7 @@ import type {Location} from 'history'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {PageFilterStateFixture} from 'sentry-fixture/pageFilters'; import {ProjectFixture} from 'sentry-fixture/project'; +import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; import {render, screen, within} from 'sentry-test/reactTestingLibrary'; @@ -49,7 +50,14 @@ describe('ScreenDetailsPage', () => { describe('Tabs', () => { beforeEach(() => { MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/events-stats/`, + url: `/organizations/${organization.slug}/events-timeseries/`, + body: { + timeSeries: [ + TimeSeriesFixture({ + yAxis: 'epm()', + }), + ], + }, }); MockApiClient.addMockResponse({ diff --git a/static/app/views/insights/queues/charts/throughputChart.spec.tsx b/static/app/views/insights/queues/charts/throughputChart.spec.tsx index 6d96de105ccba7..60c380af71840e 100644 --- a/static/app/views/insights/queues/charts/throughputChart.spec.tsx +++ b/static/app/views/insights/queues/charts/throughputChart.spec.tsx @@ -1,4 +1,5 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; +import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; import {render, screen, waitForElementToBeRemoved} from 'sentry-test/reactTestingLibrary'; @@ -11,7 +12,7 @@ jest.mock('sentry/utils/useReleaseStats'); describe('throughputChart', () => { const organization = OrganizationFixture(); - let eventsStatsMock!: jest.Mock; + let eventsTimeseriesMock!: jest.Mock; jest.mocked(useReleaseStats).mockReturnValue({ isLoading: false, @@ -22,18 +23,22 @@ describe('throughputChart', () => { }); beforeEach(() => { - eventsStatsMock = MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/events-stats/`, + eventsTimeseriesMock = MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events-timeseries/`, method: 'GET', body: { - 'queue.process': { - data: [[1739378162, [{count: 1}]]], - meta: {fields: {epm: 'rate'}, units: {epm: '1/second'}}, - }, - 'queue.publish': { - data: [[1739378162, [{count: 1}]]], - meta: {fields: {epm: 'rate'}, units: {epm: '1/second'}}, - }, + timeSeries: [ + TimeSeriesFixture({ + yAxis: 'epm()', + values: [{value: 1, timestamp: 1739378162}], + groupBy: [{key: 'span.op', value: 'queue.process'}], + }), + TimeSeriesFixture({ + yAxis: 'epm()', + values: [{value: 1, timestamp: 1739378162}], + groupBy: [{key: 'span.op', value: 'queue.publish'}], + }), + ], }, }); }); @@ -46,13 +51,13 @@ describe('throughputChart', () => { {organization} ); screen.getByText('Published vs Processed'); - expect(eventsStatsMock).toHaveBeenCalledWith( - '/organizations/org-slug/events-stats/', + expect(eventsTimeseriesMock).toHaveBeenCalledWith( + '/organizations/org-slug/events-timeseries/', expect.objectContaining({ query: expect.objectContaining({ - yAxis: 'epm()', - field: ['epm()', 'span.op'], - topEvents: '2', + yAxis: ['epm()'], + groupBy: ['span.op'], + topEvents: 2, query: 'span.op:[queue.publish, queue.process]', }), }) diff --git a/static/app/views/insights/queues/charts/throughputChart.tsx b/static/app/views/insights/queues/charts/throughputChart.tsx index 87919c3b74770c..848d9cd7050ed8 100644 --- a/static/app/views/insights/queues/charts/throughputChart.tsx +++ b/static/app/views/insights/queues/charts/throughputChart.tsx @@ -1,8 +1,7 @@ -import {useTheme} from '@emotion/react'; - import {t} from 'sentry/locale'; import type {PageFilters} from 'sentry/types/core'; import {getIntervalForTimeSeriesQuery} from 'sentry/utils/timeSeries/getIntervalForTimeSeriesQuery'; +import {useFetchSpanTimeSeries} from 'sentry/utils/timeSeries/useFetchEventsTimeSeries'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; @@ -14,10 +13,7 @@ import {BaseChartActionDropdown} from 'sentry/views/insights/common/components/c // Our loadable chart widgets use this to render, so this import is ok // eslint-disable-next-line no-restricted-imports import {InsightsLineChartWidget} from 'sentry/views/insights/common/components/insightsLineChartWidget'; -import type {DiscoverSeries} from 'sentry/views/insights/common/queries/types'; -import {useTopNSpanSeries} from 'sentry/views/insights/common/queries/useTopNDiscoverSeries'; import {getAlertsUrl} from 'sentry/views/insights/common/utils/getAlertsUrl'; -import {renameDiscoverSeries} from 'sentry/views/insights/common/utils/renameDiscoverSeries'; import type {AddToSpanDashboardOptions} from 'sentry/views/insights/common/utils/useAddToSpanDashboard'; import {useAlertsProject} from 'sentry/views/insights/common/utils/useAlertsProject'; import type {Referrer} from 'sentry/views/insights/queues/referrers'; @@ -34,7 +30,6 @@ interface Props { } export function ThroughputChart({id, error, destination, pageFilters, referrer}: Props) { - const theme = useTheme(); const organization = useOrganization(); const project = useAlertsProject(); const {selection} = usePageFilters(); @@ -55,28 +50,19 @@ export function ThroughputChart({id, error, destination, pageFilters, referrer}: data, error: topNError, isLoading, - } = useTopNSpanSeries( + } = useFetchSpanTimeSeries( { - search, + query: search, yAxis: [yAxis], - fields: [yAxis, groupBy], - topN: 2, - transformAliasToInputFormat: true, + groupBy: [groupBy], + topEvents: 2, interval, + pageFilters, }, - referrer, - pageFilters + referrer ); - const publishData: DiscoverSeries = data.find( - (d): d is DiscoverSeries => d.seriesName === 'queue.publish' - ) ?? {data: [], seriesName: 'queue.publish', meta: {fields: {}, units: {}}}; - - const processData: DiscoverSeries = data.find( - (d): d is DiscoverSeries => d.seriesName === 'queue.process' - ) ?? {data: [], seriesName: 'queue.process', meta: {fields: {}, units: {}}}; - - const colors = theme.chart.getColorPalette(2); + const timeSeries = data?.timeSeries || []; const exploreUrl = getExploreUrl({ selection, @@ -113,7 +99,7 @@ export function ThroughputChart({id, error, destination, pageFilters, referrer}: alertMenuOptions={[ { key: 'publish', - label: FIELD_ALIASES['epm() span.op:queue.publish'], + label: FIELD_ALIASES['epm() : span.op : queue.publish'], to: getAlertsUrl({ project, query: 'span.op:queue.publish', @@ -126,7 +112,7 @@ export function ThroughputChart({id, error, destination, pageFilters, referrer}: }, { key: 'process', - label: FIELD_ALIASES['epm() span.op:queue.process'], + label: FIELD_ALIASES['epm() : span.op : queue.process'], to: getAlertsUrl({ project, query: 'span.op:queue.process', @@ -148,22 +134,7 @@ export function ThroughputChart({id, error, destination, pageFilters, referrer}: id={id} extraActions={extraActions} title={title} - series={[ - renameDiscoverSeries( - { - ...publishData, - color: colors[1], - }, - 'epm() span.op:queue.publish' - ), - renameDiscoverSeries( - { - ...processData, - color: colors[2], - }, - 'epm() span.op:queue.process' - ), - ]} + timeSeries={timeSeries} aliases={FIELD_ALIASES} isLoading={isLoading} error={error ?? topNError} diff --git a/static/app/views/insights/queues/settings.ts b/static/app/views/insights/queues/settings.ts index d472efa9559d53..9b09d2ab878056 100644 --- a/static/app/views/insights/queues/settings.ts +++ b/static/app/views/insights/queues/settings.ts @@ -12,8 +12,8 @@ export const CONSUMER_QUERY_FILTER = 'span.op:queue.process'; export const PRODUCER_QUERY_FILTER = 'span.op:queue.publish'; export const FIELD_ALIASES = { - 'epm() span.op:queue.publish': t('Published'), - 'epm() span.op:queue.process': t('Processed'), + 'epm() : span.op : queue.publish': t('Published'), + 'epm() : span.op : queue.process': t('Processed'), 'avg(messaging.message.receive.latency)': t('Average Time in Queue'), 'avg(span.duration)': t('Average Processing Time'), }; diff --git a/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx b/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx index 534f1068f901dd..f94c5c810bf10b 100644 --- a/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx +++ b/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx @@ -6,6 +6,7 @@ import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; import {render, screen, waitForElementToBeRemoved} from 'sentry-test/reactTestingLibrary'; import ProjectsStore from 'sentry/stores/projectsStore'; +import {RateUnit} from 'sentry/utils/discover/fields'; import {useLocation} from 'sentry/utils/useLocation'; import usePageFilters from 'sentry/utils/usePageFilters'; import {useReleaseStats} from 'sentry/utils/useReleaseStats'; @@ -78,17 +79,31 @@ describe('destinationSummaryPage', () => { // Mock for unchanged throughput chart that still uses events-stats throughputEventsStatsMock = MockApiClient.addMockResponse({ - url: `/organizations/${organization.slug}/events-stats/`, + url: `/organizations/${organization.slug}/events-timeseries/`, method: 'GET', body: { - 'queue.process': { - data: [[1739378162, [{count: 1}]]], - meta: {fields: {epm: 'rate'}, units: {epm: '1/second'}}, - }, - 'queue.publish': { - data: [[1739378162, [{count: 1}]]], - meta: {fields: {epm: 'rate'}, units: {epm: '1/second'}}, - }, + timeSeries: [ + TimeSeriesFixture({ + yAxis: 'epm()', + meta: { + interval: 1, + valueType: 'rate', + valueUnit: RateUnit.PER_SECOND, + }, + groupBy: [{key: 'span.op', value: 'queue.process'}], + values: [{value: 1, timestamp: 1739378162000}], + }), + TimeSeriesFixture({ + yAxis: 'epm()', + meta: { + interval: 1, + valueType: 'rate', + valueUnit: RateUnit.PER_SECOND, + }, + groupBy: [{key: 'span.op', value: 'queue.publish'}], + values: [{value: 1, timestamp: 1739378162000}], + }), + ], }, match: [ MockApiClient.matchQuery({