Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type {EChartsOption} from 'echarts';
import {OrganizationFixture} from 'sentry-fixture/organization';

import {render, screen} from 'sentry-test/reactTestingLibrary';

import {AutomationStatsChart} from 'sentry/views/automations/components/automationStatsChart';

let mockChartOptions: EChartsOption | undefined;

jest.mock('echarts-for-react/lib/core', () => {
return function MockEcharts({option}: {option: EChartsOption}) {
mockChartOptions = option;
return null;
};
});

describe('AutomationStatsChart time range', () => {
const organization = OrganizationFixture();

beforeEach(() => {
MockApiClient.clearMockResponses();
mockChartOptions = undefined;
});

it('uses the time range returned by the workflow stats endpoint on a time axis', async () => {
const start = Date.parse('2026-01-13T19:00:00Z');
const end = Date.parse('2026-01-13T20:00:00Z');
MockApiClient.addMockResponse({
url: '/organizations/org-slug/workflows/123/stats/',
body: {
meta: {dataset: 'workflow', start, end},
timeSeries: [
{
yAxis: 'count()',
values: [{timestamp: end, value: 1, incomplete: false}],
meta: {interval: 60 * 60 * 1000, valueType: 'integer', valueUnit: null},
},
],
},
});

render(
<AutomationStatsChart
automationId="123"
period="1h"
start={null}
end={null}
utc={null}
/>,
{organization}
);

expect(await screen.findByText('1')).toBeInTheDocument();
expect(mockChartOptions).toMatchObject({
xAxis: {type: 'time', min: start, max: end},
});
});

it('continues to accept the legacy response during deployment', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/workflows/123/stats/',
body: [{date: '2026-01-13T20:00:00Z', count: 1}],
});

render(
<AutomationStatsChart
automationId="123"
period="1h"
start={null}
end={null}
utc={null}
/>,
{organization}
);

expect(await screen.findByText('1')).toBeInTheDocument();
});
});
85 changes: 68 additions & 17 deletions static/app/views/automations/components/automationStatsChart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useTheme} from '@emotion/react';
import styled from '@emotion/styled';
import {useQuery} from '@tanstack/react-query';

import {Container, Flex} from '@sentry/scraps/layout';

Expand All @@ -14,15 +15,53 @@ import {PanelFooter} from 'sentry/components/panels/panelFooter';
import {Placeholder} from 'sentry/components/placeholder';
import {t} from 'sentry/locale';
import type {Automation, AutomationStats} from 'sentry/types/workflowEngine/automations';
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
import {getUtcDateString} from 'sentry/utils/dates';
import {useApiQuery} from 'sentry/utils/queryClient';
import {apiOptions} from 'sentry/utils/api/apiOptions';
import {getUtcDateString, getUtcToLocalDateObject} from 'sentry/utils/dates';
import {useOrganization} from 'sentry/utils/useOrganization';
import type {TimeSeries} from 'sentry/views/dashboards/widgets/common/types';

interface IssueAlertDetailsProps extends DateTimeObject {
automationId: Automation['id'];
}

type WorkflowStatsResponse = {
meta: {
dataset: string;
end: number;
start: number;
};
timeSeries: TimeSeries[];
};

type WorkflowStatsApiResponse = AutomationStats[] | WorkflowStatsResponse;

type WorkflowStats = {
values: TimeSeries['values'];
timeframe?: WorkflowStatsResponse['meta'];
};

function normalizeWorkflowStatsResponse(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function is here temporarily while we move the endpoint to the new format. follow up to remove this normalization

response: WorkflowStatsApiResponse | undefined
): WorkflowStats | undefined {
if (!response) {
return undefined;
}

if (Array.isArray(response)) {
return {
values: response.map(({date, count}) => ({
timestamp: Date.parse(date),
value: count,
})),
};
}

return {
timeframe: response.meta,
values: response.timeSeries[0]?.values ?? [],
};
}

export function AutomationStatsChart({
automationId,
period,
Expand All @@ -34,28 +73,28 @@ export function AutomationStatsChart({
const organization = useOrganization();
const chartZoomProps = useChartZoom({saveOnZoom: true});
const {
data: fireHistory,
data: statsResponse,
isPending,
isError,
} = useApiQuery<AutomationStats[]>(
[
getApiUrl('/organizations/$organizationIdOrSlug/workflows/$workflowId/stats/', {
path: {organizationIdOrSlug: organization.slug, workflowId: automationId},
}),
} = useQuery(
apiOptions.as<WorkflowStatsApiResponse>()(
'/organizations/$organizationIdOrSlug/workflows/$workflowId/stats/',
{
path: {organizationIdOrSlug: organization.slug, workflowId: automationId},
query: {
...(period && {statsPeriod: period}),
start: start ? getUtcDateString(start) : undefined,
end: end ? getUtcDateString(end) : undefined,
utc: utc ? 'true' : undefined,
},
},
],
{staleTime: 30000}
staleTime: 30_000,
}
)
);

const stats = normalizeWorkflowStatsResponse(statsResponse);
const totalAlertsTriggered =
fireHistory?.reduce((acc, curr) => acc + curr.count, 0) ?? 0;
stats?.values.reduce((acc, curr) => acc + (curr.value ?? 0), 0) ?? 0;

return (
<Panel>
Expand All @@ -65,10 +104,14 @@ export function AutomationStatsChart({
</Container>
{isPending && <Placeholder height="200px" />}
{isError && <LoadingError />}
{fireHistory && (
{stats && (
<BarChart
{...chartZoomProps}
period={period}
showTimeInTooltip
start={start ? getUtcToLocalDateObject(start) : undefined}
end={end ? getUtcToLocalDateObject(end) : undefined}
utc={utc ?? undefined}
grid={{
left: theme.space['2xs'],
right: theme.space.xl,
Expand All @@ -78,12 +121,20 @@ export function AutomationStatsChart({
yAxis={{
minInterval: 1,
}}
xAxis={
stats.timeframe
? {
min: stats.timeframe.start,
max: stats.timeframe.end,
}
: undefined
}
series={[
{
seriesName: t('Alerts Triggered'),
data: fireHistory.map(automation => ({
name: automation.date,
value: automation.count,
data: stats.values.map(({timestamp, value}) => ({
name: timestamp,
value: value ?? 0,
Comment on lines +135 to +137

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The chart renders raw numeric timestamps on its x-axis for legacy data because isGroupedByDate is not set, causing the axis type to default to 'category' instead of 'time'.
Severity: MEDIUM

Suggested Fix

In AutomationStatsChart, when handling the legacy data path (where stats.timeframe is undefined), explicitly pass isGroupedByDate={true} to the BarChart component. This will ensure the x-axis is correctly configured as a 'time' axis, allowing it to format the numeric timestamps into human-readable dates.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: static/app/views/automations/components/automationStatsChart.tsx#L135-L137

Potential issue: In the legacy data path, when `stats.timeframe` is undefined, the
`AutomationStatsChart` component does not configure the x-axis for time-series data. The
underlying `BaseChart` defaults `isGroupedByDate` to `false`, which sets the x-axis type
to `'category'`. As the data points now use numeric timestamps instead of date strings,
the chart will render these large, unformatted numbers as category labels on the x-axis
instead of properly formatted dates. This issue is expected to occur during deployment
while both new and legacy API response formats are supported.

Did we get this right? 👍 / 👎 to inform future reviews.

})),
emphasis: {
disabled: true,
Expand Down
Loading