From b5384c5decdaf086b775ea70e6d0f10031701741 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Fri, 26 Sep 2025 16:43:49 -0400 Subject: [PATCH 1/9] migration --- .../formatters/formatTimeSeriesName.spec.tsx | 151 ------------------ .../formatters/formatTimeSeriesName.tsx | 62 +------ .../plottables/continuousTimeSeries.tsx | 15 +- .../components/insightsTimeSeriesWidget.tsx | 24 ++- .../common/utils/renameDiscoverSeries.tsx | 24 --- .../charts/responseCodeCountChart.tsx | 65 ++++---- .../http/components/httpSamplesPanel.tsx | 13 +- .../components/startDurationWidget.tsx | 23 ++- .../queues/charts/throughputChart.tsx | 52 ++---- static/app/views/insights/queues/settings.ts | 4 +- 10 files changed, 93 insertions(+), 340 deletions(-) delete mode 100644 static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.spec.tsx delete mode 100644 static/app/views/insights/common/utils/renameDiscoverSeries.tsx diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.spec.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.spec.tsx deleted file mode 100644 index d7343c1612b689..00000000000000 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.spec.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; - -import {formatTimeSeriesName} from './formatTimeSeriesName'; - -describe('formatSeriesName', () => { - describe('releases', () => { - it.each([ - ['p75(span.duration)|~|11762', 'p75(span.duration)'], - ['Releases|~|', 'Releases'], - ])('Formats %s as %s', (name, result) => { - const timeSeries = TimeSeriesFixture({ - yAxis: name, - }); - - expect(formatTimeSeriesName(timeSeries)).toEqual(result); - }); - }); - - describe('aggregates', () => { - it.each([ - ['user_misery()', 'user_misery()'], - ['apdex(200)', 'apdex(200)'], - ['p75(span.duration)', 'p75(span.duration)'], - ])('Formats %s as %s', (name, result) => { - const timeSeries = TimeSeriesFixture({ - yAxis: name, - }); - - expect(formatTimeSeriesName(timeSeries)).toEqual(result); - }); - }); - - describe('versions', () => { - it.each([ - ['frontend@31804d9a5f0b5e4f53055467cd258e1c', '31804d9a5f0b'], - ['android@5.3.1', '5.3.1'], - ['ios@5.3.1-rc1', '5.3.1-rc1'], - ])('Formats %s as %s', (name, result) => { - const timeSeries = TimeSeriesFixture({ - yAxis: name, - }); - - expect(formatTimeSeriesName(timeSeries)).toEqual(result); - }); - }); - - describe('aggregates of measurements', () => { - it.each([ - ['p75(measurements.lcp)', 'LCP'], - ['p50(measurements.lcp)', 'LCP'], - ])('Formats %s as %s', (name, result) => { - const timeSeries = TimeSeriesFixture({ - yAxis: name, - }); - - expect(formatTimeSeriesName(timeSeries)).toEqual(result); - }); - }); - - describe('equations', () => { - it.each([ - ['equation|p75(measurements.cls) + 1', 'p75(measurements.cls) + 1'], - ['equation|p75(measurements.cls)', 'p75(measurements.cls)'], - ])('Formats %s as %s', (name, result) => { - const timeSeries = TimeSeriesFixture({ - yAxis: name, - }); - - expect(formatTimeSeriesName(timeSeries)).toEqual(result); - }); - }); - - describe('combinations', () => { - it.each([ - ['equation|p75(measurements.cls) + 1|~|76123', 'p75(measurements.cls) + 1'], - ['equation|p75(measurements.cls)|~|76123', 'p75(measurements.cls)'], - ])('Formats %s as %s', (name, result) => { - const timeSeries = TimeSeriesFixture({ - yAxis: name, - }); - - expect(formatTimeSeriesName(timeSeries)).toEqual(result); - }); - }); - - describe('groupBy', () => { - it.each([ - [ - 'equation|p75(measurements.cls)|~|76123', - [{key: 'release', value: 'v0.0.2'}], - 'v0.0.2', - ], - ['p95(span.duration)', [{key: 'release', value: 'v0.0.2'}], 'v0.0.2'], - [ - 'p95(span.duration)', - [ - {key: 'release', value: 'v0.0.2'}, - {key: 'env', value: 'prod'}, - ], - 'v0.0.2,prod', - ], - [ - 'p95(span.duration)', - [{key: 'release', value: 'frontend@31804d9a5f0b5e4f53055467cd258e1c'}], - '31804d9a5f0b', - ], - [ - 'p95(span.duration)', - [{key: 'error.type', value: ['Exception', 'TypeError']}], - '["Exception","TypeError"]', - ], - [ - 'p95(span.duration)', - [{key: 'error.type', value: ['Exception', null, 'TypeError']}], - '["Exception",null,"TypeError"]', - ], - ['p95(span.duration)', [{key: 'error.type', value: [null]}], '[null]'], - ['p95(span.duration)', [{key: 'error.type', value: []}], '[]'], - [ - 'p95(span.duration)', - [ - {key: 'error.type', value: ['Exception', null]}, - {key: 'env', value: 'prod'}, - ], - '["Exception",null],prod', - ], - [ - 'p95(span.duration)', - [ - {key: 'release', value: 'v0.0.2'}, - {key: 'error.type', value: ['Exception', 'TypeError']}, - ], - 'v0.0.2,["Exception","TypeError"]', - ], - ['p95(span.duration)', [{key: 'counts', value: [1, 2, 3]}], '[1,2,3]'], - ['p95(span.duration)', [{key: 'counts', value: [1, null, 3]}], '[1,null,3]'], - [ - 'p95(span.duration)', - [{key: 'mixed', value: [null, null, null]}], - '[null,null,null]', - ], - ])('Formats %s with groupBy %s as %s', (name, groupBy, result) => { - const timeSeries = TimeSeriesFixture({ - yAxis: name, - groupBy, - }); - - expect(formatTimeSeriesName(timeSeries)).toEqual(result); - }); - }); -}); diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.tsx index 5b10a66a668e01..879a04de8a0bc8 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesName.tsx @@ -1,65 +1,15 @@ -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.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); - } + let name = `${timeSeries.yAxis}`; - return groupBy.value; + if (timeSeries.groupBy?.length) { + name += ` : ${timeSeries.groupBy + ?.map(groupBy => { + 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..25429097371d7d 100644 --- a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx +++ b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx @@ -15,6 +15,8 @@ import type { LegendSelection, TimeSeries, } 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 {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'; @@ -56,6 +58,10 @@ export interface InsightsTimeSeriesWidgetProps description?: React.ReactNode; extraActions?: React.ReactNode[]; extraPlottables?: Plottable[]; + /** + * If true, each series will be assigned a unique color rather than relying on the default colors per yAxis + */ + generateUniqueSeriesColors?: boolean; height?: string | number; interactiveTitle?: () => React.ReactNode; legendSelection?: LegendSelection | undefined; @@ -93,6 +99,7 @@ export function InsightsTimeSeriesWidget(props: InsightsTimeSeriesWidgetProps) { const {releases: releasesWithDate} = useReleaseStats(pageFiltersSelection, { enabled: props.showReleaseAs !== 'none', }); + const colors = theme.chart.getColorPalette(props.timeSeries?.length ?? 0); const releases = releasesWithDate?.map(({date, version}) => ({ timestamp: date, @@ -128,16 +135,27 @@ export function InsightsTimeSeriesWidget(props: InsightsTimeSeriesWidgetProps) { alias: aliases?.[delayedTimeSeries.yAxis], }); }), - ...(props.timeSeries?.filter(Boolean) ?? []).map(timeSeries => { + ...(props.timeSeries?.filter(Boolean) ?? []).map((timeSeries, idx) => { // TODO: After merge of ENG-5375 we don't need to run `markDelayedData` on output of `/events-timeseries/` const delayedTimeSeries = markDelayedData(timeSeries, INGESTION_DELAY); yAxes.add(timeSeries.yAxis); + let alias = aliases?.[delayedTimeSeries.yAxis]; + const plottableName = formatTimeSeriesName(delayedTimeSeries); + if (aliases?.[plottableName]) { + alias = aliases?.[plottableName]; + } + if (!alias && delayedTimeSeries.meta.isOther) { + alias = 'Other'; + } + return new PlottableDataConstructor(delayedTimeSeries, { - color: COMMON_COLORS(theme)[delayedTimeSeries.yAxis], + color: props.generateUniqueSeriesColors + ? colors[idx + 1] + : COMMON_COLORS(theme)[delayedTimeSeries.yAxis], stack: props.stacked && props.visualizationType === 'bar' ? 'all' : undefined, - alias: aliases?.[delayedTimeSeries.yAxis], + alias, }); }), ...(props.extraPlottables ?? []), 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..740de711be049a 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,38 @@ 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.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 ( 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,8 @@ 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} + generateUniqueSeriesColors 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'), }; From 94a459e427f82ca057d9ef183ea3dba732c11698 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Fri, 26 Sep 2025 16:44:18 -0400 Subject: [PATCH 2/9] format label --- .../formatters/formatTimeSeriesLabel.spec.tsx | 151 ++++++++++++++++++ .../formatters/formatTimeSeriesLabel.tsx | 65 ++++++++ 2 files changed, 216 insertions(+) create mode 100644 static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx create mode 100644 static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx new file mode 100644 index 00000000000000..233c40deaac4ad --- /dev/null +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx @@ -0,0 +1,151 @@ +import {TimeSeriesFixture} from 'sentry-fixture/timeSeries'; + +import {formatTimeSeriesLabel} from './formatTimeSeriesLabel'; + +describe('formatSeriesName', () => { + describe('releases', () => { + it.each([ + ['p75(span.duration)|~|11762', 'p75(span.duration)'], + ['Releases|~|', 'Releases'], + ])('Formats %s as %s', (name, result) => { + const timeSeries = TimeSeriesFixture({ + yAxis: name, + }); + + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); + }); + }); + + describe('aggregates', () => { + it.each([ + ['user_misery()', 'user_misery()'], + ['apdex(200)', 'apdex(200)'], + ['p75(span.duration)', 'p75(span.duration)'], + ])('Formats %s as %s', (name, result) => { + const timeSeries = TimeSeriesFixture({ + yAxis: name, + }); + + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); + }); + }); + + describe('versions', () => { + it.each([ + ['frontend@31804d9a5f0b5e4f53055467cd258e1c', '31804d9a5f0b'], + ['android@5.3.1', '5.3.1'], + ['ios@5.3.1-rc1', '5.3.1-rc1'], + ])('Formats %s as %s', (name, result) => { + const timeSeries = TimeSeriesFixture({ + yAxis: name, + }); + + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); + }); + }); + + describe('aggregates of measurements', () => { + it.each([ + ['p75(measurements.lcp)', 'LCP'], + ['p50(measurements.lcp)', 'LCP'], + ])('Formats %s as %s', (name, result) => { + const timeSeries = TimeSeriesFixture({ + yAxis: name, + }); + + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); + }); + }); + + describe('equations', () => { + it.each([ + ['equation|p75(measurements.cls) + 1', 'p75(measurements.cls) + 1'], + ['equation|p75(measurements.cls)', 'p75(measurements.cls)'], + ])('Formats %s as %s', (name, result) => { + const timeSeries = TimeSeriesFixture({ + yAxis: name, + }); + + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); + }); + }); + + describe('combinations', () => { + it.each([ + ['equation|p75(measurements.cls) + 1|~|76123', 'p75(measurements.cls) + 1'], + ['equation|p75(measurements.cls)|~|76123', 'p75(measurements.cls)'], + ])('Formats %s as %s', (name, result) => { + const timeSeries = TimeSeriesFixture({ + yAxis: name, + }); + + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); + }); + }); + + describe('groupBy', () => { + it.each([ + [ + 'equation|p75(measurements.cls)|~|76123', + [{key: 'release', value: 'v0.0.2'}], + 'v0.0.2', + ], + ['p95(span.duration)', [{key: 'release', value: 'v0.0.2'}], 'v0.0.2'], + [ + 'p95(span.duration)', + [ + {key: 'release', value: 'v0.0.2'}, + {key: 'env', value: 'prod'}, + ], + 'v0.0.2,prod', + ], + [ + 'p95(span.duration)', + [{key: 'release', value: 'frontend@31804d9a5f0b5e4f53055467cd258e1c'}], + '31804d9a5f0b', + ], + [ + 'p95(span.duration)', + [{key: 'error.type', value: ['Exception', 'TypeError']}], + '["Exception","TypeError"]', + ], + [ + 'p95(span.duration)', + [{key: 'error.type', value: ['Exception', null, 'TypeError']}], + '["Exception",null,"TypeError"]', + ], + ['p95(span.duration)', [{key: 'error.type', value: [null]}], '[null]'], + ['p95(span.duration)', [{key: 'error.type', value: []}], '[]'], + [ + 'p95(span.duration)', + [ + {key: 'error.type', value: ['Exception', null]}, + {key: 'env', value: 'prod'}, + ], + '["Exception",null],prod', + ], + [ + 'p95(span.duration)', + [ + {key: 'release', value: 'v0.0.2'}, + {key: 'error.type', value: ['Exception', 'TypeError']}, + ], + 'v0.0.2,["Exception","TypeError"]', + ], + ['p95(span.duration)', [{key: 'counts', value: [1, 2, 3]}], '[1,2,3]'], + ['p95(span.duration)', [{key: 'counts', value: [1, null, 3]}], '[1,null,3]'], + [ + 'p95(span.duration)', + [{key: 'mixed', value: [null, null, null]}], + '[null,null,null]', + ], + ])('Formats %s with groupBy %s as %s', (name, groupBy, result) => { + const timeSeries = TimeSeriesFixture({ + yAxis: name, + groupBy, + }); + + expect(formatTimeSeriesLabel(timeSeries)).toEqual(result); + }); + }); +}); 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..b5295469d44ea0 --- /dev/null +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx @@ -0,0 +1,65 @@ +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.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; +} From effed6db7c1c20ecd4e16585d3c03c98aa2fc5bd Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Fri, 26 Sep 2025 17:05:13 -0400 Subject: [PATCH 3/9] fix tests --- .../http/components/httpSamplesPanel.spec.tsx | 61 ++++++++----------- .../views/screenSummaryPage.spec.tsx | 10 ++- .../screens/views/screenDetailsPage.spec.tsx | 10 ++- .../views/destinationSummaryPage.spec.tsx | 33 +++++++--- 4 files changed, 67 insertions(+), 47 deletions(-) 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/mobile/appStarts/views/screenSummaryPage.spec.tsx b/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.spec.tsx index 106f0f55d70105..526baf9375b848 100644 --- a/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.spec.tsx +++ b/static/app/views/insights/mobile/appStarts/views/screenSummaryPage.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, waitFor, within} from 'sentry-test/reactTestingLibrary'; @@ -67,7 +68,14 @@ describe('Screen Summary', () => { ], }); 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/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({ From 3d79196fa7ca69da9db930a3fa1ae435ef4a6489 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 1 Oct 2025 10:27:01 -0400 Subject: [PATCH 4/9] fix test --- .../queues/charts/throughputChart.spec.tsx | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) 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]', }), }) From a9fddca61654c9de8ff90bfeb86f8ba1ed64a7bc Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Wed, 1 Oct 2025 10:39:45 -0400 Subject: [PATCH 5/9] tsc --- .../insights/common/components/insightsTimeSeriesWidget.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx index 25429097371d7d..7c5b31e512da1b 100644 --- a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx +++ b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx @@ -15,7 +15,6 @@ import type { LegendSelection, TimeSeries, } 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 {Area} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/area'; import {Bars} from 'sentry/views/dashboards/widgets/timeSeriesWidget/plottables/bars'; From 3bb820e6435d165f771ecfe1f4e25ed58c1752fb Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Thu, 2 Oct 2025 11:07:25 -0400 Subject: [PATCH 6/9] fix --- package.json | 4 ++-- .../formatters/formatTimeSeriesLabel.tsx | 2 +- .../common/components/insightsTimeSeriesWidget.tsx | 13 ++++--------- .../components/charts/responseCodeCountChart.tsx | 1 - .../insights/queues/charts/throughputChart.tsx | 1 - 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 6d89c4f841a51f..b5d6a0b4b7e419 100644 --- a/package.json +++ b/package.json @@ -210,11 +210,11 @@ "eslint-plugin-no-relative-import-paths": "^1.6.1", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "5.2.0", + "eslint-plugin-react-you-might-not-need-an-effect": "0.5.3", "eslint-plugin-sentry": "^2.10.0", "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-typescript-sort-keys": "^3.3.0", "eslint-plugin-unicorn": "^57.0.0", - "eslint-plugin-react-you-might-not-need-an-effect": "0.5.3", "expect-type": "1.2.1", "globals": "16.3.0", "jest": "30.0.4", @@ -301,5 +301,5 @@ "current node" ] }, - "packageManager": "pnpm@10.10.0" + "packageManager": "pnpm@10.17.1" } diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx index dd410ec9617456..9037f6893a6ce5 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.tsx @@ -9,7 +9,7 @@ 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 { +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. diff --git a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx index 7c5b31e512da1b..50398d5087baf8 100644 --- a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx +++ b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx @@ -60,7 +60,6 @@ export interface InsightsTimeSeriesWidgetProps /** * If true, each series will be assigned a unique color rather than relying on the default colors per yAxis */ - generateUniqueSeriesColors?: boolean; height?: string | number; interactiveTitle?: () => React.ReactNode; legendSelection?: LegendSelection | undefined; @@ -98,7 +97,6 @@ export function InsightsTimeSeriesWidget(props: InsightsTimeSeriesWidgetProps) { const {releases: releasesWithDate} = useReleaseStats(pageFiltersSelection, { enabled: props.showReleaseAs !== 'none', }); - const colors = theme.chart.getColorPalette(props.timeSeries?.length ?? 0); const releases = releasesWithDate?.map(({date, version}) => ({ timestamp: date, @@ -134,7 +132,7 @@ export function InsightsTimeSeriesWidget(props: InsightsTimeSeriesWidgetProps) { alias: aliases?.[delayedTimeSeries.yAxis], }); }), - ...(props.timeSeries?.filter(Boolean) ?? []).map((timeSeries, idx) => { + ...(props.timeSeries?.filter(Boolean) ?? []).map(timeSeries => { // TODO: After merge of ENG-5375 we don't need to run `markDelayedData` on output of `/events-timeseries/` const delayedTimeSeries = markDelayedData(timeSeries, INGESTION_DELAY); @@ -145,14 +143,9 @@ export function InsightsTimeSeriesWidget(props: InsightsTimeSeriesWidgetProps) { if (aliases?.[plottableName]) { alias = aliases?.[plottableName]; } - if (!alias && delayedTimeSeries.meta.isOther) { - alias = 'Other'; - } return new PlottableDataConstructor(delayedTimeSeries, { - color: props.generateUniqueSeriesColors - ? colors[idx + 1] - : COMMON_COLORS(theme)[delayedTimeSeries.yAxis], + color: COMMON_COLORS(theme)[plottableName], stack: props.stacked && props.visualizationType === 'bar' ? 'all' : undefined, alias, }); @@ -305,5 +298,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/http/components/charts/responseCodeCountChart.tsx b/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx index 740de711be049a..b39f06fba7e204 100644 --- a/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx +++ b/static/app/views/insights/http/components/charts/responseCodeCountChart.tsx @@ -106,7 +106,6 @@ export function ResponseCodeCountChart({ Date: Thu, 2 Oct 2025 11:11:39 -0400 Subject: [PATCH 7/9] revert --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b5d6a0b4b7e419..6d89c4f841a51f 100644 --- a/package.json +++ b/package.json @@ -210,11 +210,11 @@ "eslint-plugin-no-relative-import-paths": "^1.6.1", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "5.2.0", - "eslint-plugin-react-you-might-not-need-an-effect": "0.5.3", "eslint-plugin-sentry": "^2.10.0", "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-typescript-sort-keys": "^3.3.0", "eslint-plugin-unicorn": "^57.0.0", + "eslint-plugin-react-you-might-not-need-an-effect": "0.5.3", "expect-type": "1.2.1", "globals": "16.3.0", "jest": "30.0.4", @@ -301,5 +301,5 @@ "current node" ] }, - "packageManager": "pnpm@10.17.1" + "packageManager": "pnpm@10.10.0" } From 1561aad5ce8607d53106220c12286c5d99453e2b Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Thu, 2 Oct 2025 11:12:17 -0400 Subject: [PATCH 8/9] fix --- .../insights/common/components/insightsTimeSeriesWidget.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx index 50398d5087baf8..d22c68df7ab28e 100644 --- a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx +++ b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx @@ -57,9 +57,6 @@ export interface InsightsTimeSeriesWidgetProps description?: React.ReactNode; extraActions?: React.ReactNode[]; extraPlottables?: Plottable[]; - /** - * If true, each series will be assigned a unique color rather than relying on the default colors per yAxis - */ height?: string | number; interactiveTitle?: () => React.ReactNode; legendSelection?: LegendSelection | undefined; From 3bd85edd2413978cf8582d47656bcabd575d18a3 Mon Sep 17 00:00:00 2001 From: Dominik Buszowiecki Date: Thu, 2 Oct 2025 11:37:33 -0400 Subject: [PATCH 9/9] fix --- .../timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx index 9c48f12cadc887..18e9c773909a78 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/formatters/formatTimeSeriesLabel.spec.tsx @@ -157,7 +157,7 @@ describe('formatSeriesName', () => { isOther: true, }; - expect(formatTimeSeriesName(timeSeries)).toBe('Other'); + expect(formatTimeSeriesLabel(timeSeries)).toBe('Other'); }); }); });