-
-
Notifications
You must be signed in to change notification settings - Fork 4.9k
feat(explore): Show agent data without conversations #124279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
obostjancic
merged 14 commits into
master
from
ognjenbostjancic/tet-2970-show-agentic-traces-non-conversations-in-exploreagents
Sep 15, 2026
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
1f71dc3
feat(explore): Show agent data without conversations
obostjancic 131db5a
fix(explore): Refine agent chart cards
obostjancic 6b54b4a
fix(explore): Remove agent chart breakdown footers
obostjancic dd1afe2
fix(explore): Scale agent chart granularity
obostjancic 9396967
fix(explore): Hide agent tabs during onboarding
obostjancic 05952ad
fix(explore): Add agent chart interval selector
obostjancic 768c8a9
Merge branch 'master' into ognjenbostjancic/tet-2970-show-agentic-tra…
obostjancic e122c9b
ref(explore): Remove replaced conversations chart
obostjancic 525ab99
feat(explore): Gate unified Agents overview
obostjancic 8564d61
Merge remote-tracking branch 'origin/master' into ognjenbostjancic/te…
obostjancic 5fdbad3
Merge branch 'master' into ognjenbostjancic/tet-2970-show-agentic-tra…
obostjancic 6f12ad7
fix(explore): Show conversation setup without agent data
obostjancic bf3d53e
Merge remote-tracking branch 'origin/ognjenbostjancic/tet-2970-show-a…
obostjancic dca0d9b
fix(explore): Match agent spans to LLM query columns
obostjancic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
209 changes: 209 additions & 0 deletions
209
static/app/views/explore/conversations/components/agentsCharts.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| import {useMemo} from 'react'; | ||
|
|
||
| import {CompactSelect} from '@sentry/scraps/compactSelect'; | ||
| import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout'; | ||
| import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; | ||
| import {Tooltip} from '@sentry/scraps/tooltip'; | ||
|
|
||
| import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; | ||
| import {IconClock} from 'sentry/icons'; | ||
| import {t} from 'sentry/locale'; | ||
| import { | ||
| ChartIntervalUnspecifiedStrategy, | ||
| useChartInterval, | ||
| } from 'sentry/utils/useChartInterval'; | ||
| import {useLocation} from 'sentry/utils/useLocation'; | ||
| import {useNavigate} from 'sentry/utils/useNavigate'; | ||
| import {useOrganization} from 'sentry/utils/useOrganization'; | ||
| import { | ||
| type DashboardDetails, | ||
| DisplayType, | ||
| type Widget, | ||
| WidgetType, | ||
| } from 'sentry/views/dashboards/types'; | ||
| import WidgetCard from 'sentry/views/dashboards/widgetCard'; | ||
| import {DashboardsMEPProvider} from 'sentry/views/dashboards/widgetCard/dashboardsMEPContext'; | ||
| import {WidgetLegendSelectionState} from 'sentry/views/dashboards/widgetLegendSelectionState'; | ||
| import {useCombinedQuery} from 'sentry/views/insights/pages/agents/hooks/useCombinedQuery'; | ||
| import { | ||
| getAgentRunsFilter, | ||
| getToolSpansFilter, | ||
| } from 'sentry/views/insights/pages/agents/utils/query'; | ||
| import {SpanFields} from 'sentry/views/insights/types'; | ||
|
|
||
| const AI_CLIENT_FILTER = `${SpanFields.GEN_AI_OPERATION_TYPE}:ai_client`; | ||
| const COUNT = `count(${SpanFields.SPAN_DURATION})`; | ||
| const COST = `sum(${SpanFields.GEN_AI_COST_TOTAL_TOKENS})`; | ||
|
|
||
| function createGroupedWidget({ | ||
| id, | ||
| title, | ||
| description, | ||
| query, | ||
| groupBy, | ||
| aggregate, | ||
| interval, | ||
| groupByLabel, | ||
| aggregateLabel, | ||
| }: { | ||
| aggregate: string; | ||
| aggregateLabel: string; | ||
| description: string; | ||
| groupBy: string; | ||
| groupByLabel: string; | ||
| id: string; | ||
| interval: string; | ||
| query: string; | ||
| title: string; | ||
| }): Widget { | ||
| return { | ||
| id, | ||
| title, | ||
| description, | ||
| displayType: DisplayType.BAR, | ||
| widgetType: WidgetType.SPANS, | ||
| interval, | ||
| limit: 3, | ||
| queries: [ | ||
| { | ||
| name: '', | ||
| conditions: query, | ||
| fields: [groupBy, aggregate], | ||
| aggregates: [aggregate], | ||
| columns: [groupBy], | ||
| fieldAliases: [groupByLabel, aggregateLabel], | ||
| orderby: `-${aggregate}`, | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| export function AgentsCharts() { | ||
| const organization = useOrganization(); | ||
| const location = useLocation(); | ||
| const navigate = useNavigate(); | ||
| const {selection} = usePageFilters(); | ||
| const [chartInterval, setChartInterval, chartIntervalOptions] = useChartInterval({ | ||
| unspecifiedStrategy: ChartIntervalUnspecifiedStrategy.USE_BIGGEST, | ||
| }); | ||
| const chartIntervalLabel = | ||
| chartIntervalOptions.find(({value}) => value === chartInterval)?.label ?? | ||
| chartInterval; | ||
|
|
||
| const agentRunsQuery = useCombinedQuery(getAgentRunsFilter()); | ||
| const estimatedCostQuery = useCombinedQuery(AI_CLIENT_FILTER); | ||
| const toolCallsQuery = useCombinedQuery(getToolSpansFilter()); | ||
|
|
||
| const widgets = useMemo<Widget[]>( | ||
| () => [ | ||
| createGroupedWidget({ | ||
| id: 'explore-agents-agent-runs', | ||
| interval: chartInterval, | ||
| title: t('Agent runs'), | ||
| description: t('Number of agent runs grouped by agent name.'), | ||
| query: agentRunsQuery, | ||
| groupBy: SpanFields.GEN_AI_AGENT_NAME, | ||
| aggregate: COUNT, | ||
| groupByLabel: t('Agent Name'), | ||
| aggregateLabel: t('Runs'), | ||
| }), | ||
| createGroupedWidget({ | ||
| id: 'explore-agents-estimated-cost', | ||
| interval: chartInterval, | ||
| title: t('Estimated Cost'), | ||
| description: t('Estimated cost of LLM calls grouped by response model.'), | ||
| query: estimatedCostQuery, | ||
| groupBy: SpanFields.GEN_AI_RESPONSE_MODEL, | ||
| aggregate: COST, | ||
| groupByLabel: t('Model'), | ||
| aggregateLabel: t('Estimated Cost'), | ||
| }), | ||
| createGroupedWidget({ | ||
| id: 'explore-agents-tool-calls', | ||
| interval: chartInterval, | ||
| title: t('Tool calls'), | ||
| description: t('Number of tool calls grouped by tool name.'), | ||
| query: toolCallsQuery, | ||
| groupBy: SpanFields.GEN_AI_TOOL_NAME, | ||
| aggregate: COUNT, | ||
| groupByLabel: t('Tool Name'), | ||
| aggregateLabel: t('Calls'), | ||
| }), | ||
| ], | ||
| [agentRunsQuery, chartInterval, estimatedCostQuery, toolCallsQuery] | ||
| ); | ||
|
|
||
| const dashboard = useMemo<DashboardDetails>( | ||
| () => ({ | ||
| id: 'explore-agents', | ||
| title: t('Agents'), | ||
| dateCreated: '', | ||
| filters: {}, | ||
| projects: undefined, | ||
| widgets, | ||
| }), | ||
| [widgets] | ||
| ); | ||
|
|
||
| const widgetLegendState = useMemo( | ||
| () => | ||
| new WidgetLegendSelectionState({ | ||
| dashboard, | ||
| location, | ||
| navigate, | ||
| organization, | ||
| }), | ||
| [dashboard, location, navigate, organization] | ||
| ); | ||
|
|
||
| return ( | ||
| <Container containerType="inline-size"> | ||
| <Stack gap="sm"> | ||
| <Flex justify="end"> | ||
| <Tooltip title={t('Time interval displayed in the charts')}> | ||
| <CompactSelect | ||
| trigger={triggerProps => ( | ||
| <OverlayTrigger.Button | ||
| {...triggerProps} | ||
| aria-label={t('Chart interval: %s', chartIntervalLabel)} | ||
| icon={<IconClock />} | ||
| size="xs" | ||
| variant="transparent" | ||
| > | ||
| {chartIntervalLabel} | ||
| </OverlayTrigger.Button> | ||
| )} | ||
| menuTitle={t('Interval')} | ||
| options={chartIntervalOptions} | ||
| value={chartInterval} | ||
| onChange={option => setChartInterval(option.value)} | ||
| /> | ||
| </Tooltip> | ||
| </Flex> | ||
| <Grid | ||
| columns={{zero: 'minmax(0, 1fr)', xl: 'repeat(3, minmax(0, 1fr))'}} | ||
| gap="md" | ||
| > | ||
| {widgets.map(widget => ( | ||
| <Container key={widget.id} minHeight="240px" minWidth="0"> | ||
| <DashboardsMEPProvider> | ||
| <WidgetCard | ||
| disableFullscreen | ||
| disableTableActions | ||
| disableZoom | ||
| dashboardFilters={dashboard.filters} | ||
| selection={selection} | ||
| showContextMenu={false} | ||
| widget={widget} | ||
| widgetInterval={chartInterval} | ||
| widgetLegendState={widgetLegendState} | ||
| widgetLimitReached={false} | ||
| /> | ||
| </DashboardsMEPProvider> | ||
| </Container> | ||
| ))} | ||
| </Grid> | ||
| </Stack> | ||
| </Container> | ||
| ); | ||
| } | ||
97 changes: 97 additions & 0 deletions
97
static/app/views/explore/conversations/components/agentsTable.spec.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import {OrganizationFixture} from 'sentry-fixture/organization'; | ||
| import {ProjectFixture} from 'sentry-fixture/project'; | ||
|
|
||
| import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary'; | ||
|
|
||
| import {PageFiltersStore} from 'sentry/components/pageFilters/store'; | ||
| import {ProjectsStore} from 'sentry/stores/projectsStore'; | ||
|
|
||
| import {AgentsTable} from './agentsTable'; | ||
|
|
||
| const organization = OrganizationFixture(); | ||
| const project = ProjectFixture({id: '1'}); | ||
|
|
||
| const LLM_CALLS_FIELDS = [ | ||
| 'id', | ||
| 'gen_ai.output.messages', | ||
| 'gen_ai.response.model', | ||
| 'gen_ai.cost.total_tokens', | ||
| 'timestamp', | ||
| ]; | ||
|
|
||
| describe('AgentsTable', () => { | ||
| beforeEach(() => { | ||
| PageFiltersStore.init(); | ||
| PageFiltersStore.onInitializeUrlState({ | ||
| projects: [Number(project.id)], | ||
| environments: [], | ||
| datetime: {period: '14d', start: null, end: null, utc: null}, | ||
| }); | ||
| ProjectsStore.loadInitialData([project]); | ||
| MockApiClient.addMockResponse({ | ||
| url: `/organizations/${organization.slug}/events/validate/`, | ||
| body: { | ||
| dataset: [], | ||
| environment: [], | ||
| field: [], | ||
| orderby: [], | ||
| projects: [], | ||
| query: {error: null, fields: [], valid: true}, | ||
| valid: true, | ||
| }, | ||
| }); | ||
| MockApiClient.addMockResponse({ | ||
| url: `/organizations/${organization.slug}/trace-items/attributes/`, | ||
| body: [], | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| PageFiltersStore.reset(); | ||
| ProjectsStore.reset(); | ||
| MockApiClient.clearMockResponses(); | ||
| }); | ||
|
|
||
| it('uses the LLM Calls saved query columns for agent spans', async () => { | ||
| const spansRequest = MockApiClient.addMockResponse({ | ||
| url: `/organizations/${organization.slug}/events/`, | ||
| body: {data: [], meta: {dataScanned: 'full'}}, | ||
| }); | ||
|
|
||
| render( | ||
| <AgentsTable | ||
| activeTab="spans" | ||
| hasAgenticSpans | ||
| hasConversations={false} | ||
| onConversationOnboardingDismiss={jest.fn()} | ||
| onTabChange={jest.fn()} | ||
| />, | ||
| { | ||
| organization, | ||
| initialRouterConfig: { | ||
| location: { | ||
| pathname: `/organizations/${organization.slug}/explore/agents/`, | ||
| }, | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| expect(await screen.findByTestId('spans-table')).toBeInTheDocument(); | ||
| await waitFor(() => | ||
| expect(spansRequest).toHaveBeenCalledWith( | ||
| `/organizations/${organization.slug}/events/`, | ||
| expect.objectContaining({ | ||
| query: expect.objectContaining({ | ||
| field: expect.arrayContaining(LLM_CALLS_FIELDS), | ||
| }), | ||
| }) | ||
| ) | ||
| ); | ||
| expect(spansRequest).not.toHaveBeenCalledWith( | ||
| `/organizations/${organization.slug}/events/`, | ||
| expect.objectContaining({ | ||
| query: expect.objectContaining({field: expect.arrayContaining(['span.name'])}), | ||
| }) | ||
| ); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.