From 086617d2e453e8792741181148de47f484a9b599 Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Mon, 13 Apr 2026 17:02:23 +0600 Subject: [PATCH 01/13] feat(source-map-config-issues): Implementing Impact section in issue details --- .../sourceMapIssues/impactSection.tsx | 142 +++++++++++++++ .../sourceMapIssues/sourceMapIssueDetails.tsx | 3 + .../useProcessingErrorsQuery.ts | 161 ++++++++++++++++++ 3 files changed, 306 insertions(+) create mode 100644 static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx create mode 100644 static/app/views/issueDetails/configurationIssues/sourceMapIssues/useProcessingErrorsQuery.ts diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx new file mode 100644 index 00000000000000..2a190ed461f81e --- /dev/null +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx @@ -0,0 +1,142 @@ +import React from 'react'; + +import {Flex, Stack} from '@sentry/scraps/layout'; +import {Link} from '@sentry/scraps/link'; +import {Heading, Text} from '@sentry/scraps/text'; + +import {LoadingError} from 'sentry/components/loadingError'; +import {LoadingIndicator} from 'sentry/components/loadingIndicator'; +import {TimeSince} from 'sentry/components/timeSince'; +import {Version} from 'sentry/components/version'; +import {t, tn} from 'sentry/locale'; +import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; +import {useOrganization} from 'sentry/utils/useOrganization'; +import {SectionDivider} from 'sentry/views/issueDetails/streamline/foldSection'; + +import { + useAffectedReleasesQuery, + useProcessingErrorsQuery, + useSampleEventsQuery, +} from './useProcessingErrorsQuery'; + +interface ImpactSectionProps { + projectId: string; +} + +export function ImpactSection({projectId}: ImpactSectionProps) { + const organization = useOrganization(); + const {count, isLoading, isError} = useProcessingErrorsQuery({projectId}); + const { + releases, + isLoading: releasesLoading, + isError: releasesError, + } = useAffectedReleasesQuery({projectId}); + const { + events, + isLoading: eventsLoading, + isError: eventsError, + } = useSampleEventsQuery({projectId}); + + function renderCount() { + if (isLoading) { + return ; + } + if (isError) { + return ; + } + if (count === null) { + return null; + } + return ( + + {tn( + '%s event with unreadable stack traces in the last 30 days', + '%s events with unreadable stack traces in the last 30 days', + count + )} + + ); + } + + function renderReleases() { + if (releasesLoading) { + return ; + } + if (releasesError) { + return ; + } + if (releases.length === 0) { + return null; + } + return ( + + {t('Affected releases')} + + {releases.map(({release, count: eventCount}) => ( + + + · + {tn('%s event', '%s events', eventCount)} + + ))} + + + ); + } + + function renderSampleEvents() { + if (eventsLoading) { + return ; + } + if (eventsError) { + return ; + } + if (events.length === 0) { + return null; + } + return ( + + {t('Sample events')} + + {events.map(({eventId, groupId, title, timestamp}) => ( + + + {title} + + · + + + + + ))} + + + ); + } + + const releasesContent = renderReleases(); + const sampleEventsContent = renderSampleEvents(); + + return ( + + {t('Impact')} + {renderCount()} + {releasesContent && ( + + + {releasesContent} + + )} + {sampleEventsContent && ( + + + {sampleEventsContent} + + )} + + ); +} diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/sourceMapIssueDetails.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/sourceMapIssueDetails.tsx index ea0afaacf9afa7..a5fcd87e655137 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/sourceMapIssueDetails.tsx +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/sourceMapIssueDetails.tsx @@ -5,6 +5,7 @@ import type {Project} from 'sentry/types/project'; import {SectionDivider} from 'sentry/views/issueDetails/streamline/foldSection'; import {DiagnosisSection} from './diagnosisSection'; +import {ImpactSection} from './impactSection'; import {ProblemSection} from './problemSection'; import {TroubleshootingSection} from './troubleshootingSection'; @@ -28,6 +29,8 @@ export function SourceMapIssueDetails({event, project}: SourceMapIssueDetailsPro + + ); } diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/useProcessingErrorsQuery.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/useProcessingErrorsQuery.ts new file mode 100644 index 00000000000000..ca432378e2eafa --- /dev/null +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/useProcessingErrorsQuery.ts @@ -0,0 +1,161 @@ +import {getApiUrl} from 'sentry/utils/api/getApiUrl'; +import {useApiQuery} from 'sentry/utils/queryClient'; +import {useOrganization} from 'sentry/utils/useOrganization'; + +const COUNT_FIELD = 'count_unique(event_id)' as const; +const RELEASE_FIELDS = ['release', COUNT_FIELD] as const; +const SAMPLE_FIELDS = ['title', 'event_id', 'group_id', 'timestamp'] as const; + +interface ProcessingErrorsCountResult { + data: Array>; +} + +type ReleaseRow = {release: string} & Record; + +interface ProcessingErrorsReleasesResult { + data: ReleaseRow[]; +} + +export interface ProcessingErrorsCount { + count: number | null; + isError: boolean; + isLoading: boolean; +} + +export interface AffectedRelease { + count: number; + release: string; +} + +export interface SampleEvent { + eventId: string; + groupId: string; + timestamp: string; + title: string; +} + +export interface SampleEventsResult { + events: SampleEvent[]; + isError: boolean; + isLoading: boolean; +} + +export interface AffectedReleasesResult { + isError: boolean; + isLoading: boolean; + releases: AffectedRelease[]; +} + +interface UseProcessingErrorsQueryOptions { + projectId: string; +} + +export function useProcessingErrorsQuery({ + projectId, +}: UseProcessingErrorsQueryOptions): ProcessingErrorsCount { + const organization = useOrganization(); + + const {data, isLoading, isError} = useApiQuery( + [ + getApiUrl('/organizations/$organizationIdOrSlug/events/', { + path: {organizationIdOrSlug: organization.slug}, + }), + { + query: { + dataset: 'processing_errors', + field: [COUNT_FIELD], + statsPeriod: '30d', + project: projectId, + referrer: 'api.issues.sourcemap-configuration.impact', + }, + }, + ], + {staleTime: 60_000} + ); + + return { + count: data?.data?.[0]?.[COUNT_FIELD] ?? null, + isLoading, + isError, + }; +} + +export function useAffectedReleasesQuery({ + projectId, +}: UseProcessingErrorsQueryOptions): AffectedReleasesResult { + const organization = useOrganization(); + + const {data, isLoading, isError} = useApiQuery( + [ + getApiUrl('/organizations/$organizationIdOrSlug/events/', { + path: {organizationIdOrSlug: organization.slug}, + }), + { + query: { + dataset: 'processing_errors', + field: RELEASE_FIELDS, + sort: `-${COUNT_FIELD}`, + per_page: 5, + statsPeriod: '30d', + project: projectId, + referrer: 'api.issues.sourcemap-configuration.impact-releases', + }, + }, + ], + {staleTime: 60_000} + ); + + const rows: ReleaseRow[] = data?.data ?? []; + const releases: AffectedRelease[] = rows + .filter((row: ReleaseRow) => Boolean(row.release)) + .map((row: ReleaseRow) => ({release: row.release, count: row[COUNT_FIELD]})); + + return {releases, isLoading, isError}; +} + +type SampleRow = { + event_id: string; + group_id: string; + timestamp: string; + title: string; +}; + +interface ProcessingErrorsSamplesResult { + data: SampleRow[]; +} + +export function useSampleEventsQuery({ + projectId, +}: UseProcessingErrorsQueryOptions): SampleEventsResult { + const organization = useOrganization(); + + const {data, isLoading, isError} = useApiQuery( + [ + getApiUrl('/organizations/$organizationIdOrSlug/events/', { + path: {organizationIdOrSlug: organization.slug}, + }), + { + query: { + dataset: 'processing_errors', + field: SAMPLE_FIELDS, + sort: '-timestamp', + per_page: 5, + statsPeriod: '30d', + project: projectId, + referrer: 'api.issues.sourcemap-configuration.impact-samples', + }, + }, + ], + {staleTime: 60_000} + ); + + const rows: SampleRow[] = data?.data ?? []; + const events: SampleEvent[] = rows.map((row: SampleRow) => ({ + title: row.title, + eventId: row.event_id, + groupId: row.group_id, + timestamp: row.timestamp, + })); + + return {events, isLoading, isError}; +} From 4a5b7f2a096e9476f4fb78d52f011c0f7a832701 Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Mon, 13 Apr 2026 18:38:24 +0600 Subject: [PATCH 02/13] feat(source-map-config-issues): Iterating --- .../sourceMapIssues/impactSection.tsx | 154 ++++++++--------- .../queries/useAffectedReleases.ts | 59 +++++++ .../queries/useImpactedEventsCount.ts | 46 +++++ .../queries/useSampleEvents.ts | 66 +++++++ .../sourceMapIssues/sourceMapIssueDetails.tsx | 2 +- .../useProcessingErrorsQuery.ts | 161 ------------------ 6 files changed, 242 insertions(+), 246 deletions(-) create mode 100644 static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts create mode 100644 static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts create mode 100644 static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts delete mode 100644 static/app/views/issueDetails/configurationIssues/sourceMapIssues/useProcessingErrorsQuery.ts diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx index 2a190ed461f81e..ce8510d1755554 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import {Fragment} from 'react'; import {Flex, Stack} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; @@ -9,92 +9,85 @@ import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {TimeSince} from 'sentry/components/timeSince'; import {Version} from 'sentry/components/version'; import {t, tn} from 'sentry/locale'; +import type {Project} from 'sentry/types/project'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useOrganization} from 'sentry/utils/useOrganization'; import {SectionDivider} from 'sentry/views/issueDetails/streamline/foldSection'; -import { - useAffectedReleasesQuery, - useProcessingErrorsQuery, - useSampleEventsQuery, -} from './useProcessingErrorsQuery'; +import {useAffectedReleases} from './queries/useAffectedReleases'; +import {useImpactedEventsCount} from './queries/useImpactedEventsCount'; +import {useSampleEvents} from './queries/useSampleEvents'; -interface ImpactSectionProps { - projectId: string; +function EventsCount({project}: {project: Project}) { + const {count, isLoading, isError} = useImpactedEventsCount({project}); + + if (isLoading) { + return ; + } + if (isError) { + return ; + } + if (count === null) { + return null; + } + return ( + + {tn( + '%s event with unreadable stack traces in the last 30 days', + '%s events with unreadable stack traces in the last 30 days', + count + )} + + ); } -export function ImpactSection({projectId}: ImpactSectionProps) { - const organization = useOrganization(); - const {count, isLoading, isError} = useProcessingErrorsQuery({projectId}); - const { - releases, - isLoading: releasesLoading, - isError: releasesError, - } = useAffectedReleasesQuery({projectId}); - const { - events, - isLoading: eventsLoading, - isError: eventsError, - } = useSampleEventsQuery({projectId}); +function AffectedReleases({project}: {project: Project}) { + const {releases, isLoading, isError} = useAffectedReleases({project}); - function renderCount() { - if (isLoading) { - return ; - } - if (isError) { - return ; - } - if (count === null) { - return null; - } - return ( - - {tn( - '%s event with unreadable stack traces in the last 30 days', - '%s events with unreadable stack traces in the last 30 days', - count - )} - - ); + if (isLoading) { + return ; } - - function renderReleases() { - if (releasesLoading) { - return ; - } - if (releasesError) { - return ; - } - if (releases.length === 0) { - return null; - } - return ( + if (isError) { + return ; + } + if (releases.length === 0) { + return null; + } + return ( + + {t('Affected releases')} - {releases.map(({release, count: eventCount}) => ( + {releases.map(({release, count}) => ( · - {tn('%s event', '%s events', eventCount)} + {tn('%s event', '%s events', count)} ))} - ); - } + + ); +} + +function SampleEvents({project}: {project: Project}) { + const organization = useOrganization(); + const {events, isLoading, isError} = useSampleEvents({project}); - function renderSampleEvents() { - if (eventsLoading) { - return ; - } - if (eventsError) { - return ; - } - if (events.length === 0) { - return null; - } - return ( + if (isLoading) { + return ; + } + if (isError) { + return ; + } + if (events.length === 0) { + return null; + } + return ( + + {t('Sample events')} @@ -115,28 +108,21 @@ export function ImpactSection({projectId}: ImpactSectionProps) { ))} - ); - } + + ); +} - const releasesContent = renderReleases(); - const sampleEventsContent = renderSampleEvents(); +interface ImpactSectionProps { + project: Project; +} +export function ImpactSection({project}: ImpactSectionProps) { return ( {t('Impact')} - {renderCount()} - {releasesContent && ( - - - {releasesContent} - - )} - {sampleEventsContent && ( - - - {sampleEventsContent} - - )} + + + ); } diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts new file mode 100644 index 00000000000000..b231c1ecc8faf5 --- /dev/null +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts @@ -0,0 +1,59 @@ +import type {Project} from 'sentry/types/project'; +import {getApiUrl} from 'sentry/utils/api/getApiUrl'; +import {useApiQuery} from 'sentry/utils/queryClient'; +import {useOrganization} from 'sentry/utils/useOrganization'; + +type ReleaseRow = {release: string} & {'count_unique(event_id)': number}; + +interface ReleasesResult { + data: ReleaseRow[]; +} + +export interface AffectedRelease { + count: number; + release: string; +} + +export interface AffectedReleasesResult { + isError: boolean; + isLoading: boolean; + releases: AffectedRelease[]; +} + +interface Options { + project: Project; +} + +export function useAffectedReleases({project}: Options): AffectedReleasesResult { + const organization = useOrganization(); + + const {data, isLoading, isError} = useApiQuery( + [ + getApiUrl('/organizations/$organizationIdOrSlug/events/', { + path: {organizationIdOrSlug: organization.slug}, + }), + { + query: { + dataset: 'processing_errors', + field: ['release', 'count_unique(event_id)'], + sort: '-count_unique(event_id)', + per_page: 5, + statsPeriod: '30d', + project: project.id, + referrer: 'api.issues.sourcemap-configuration.impact-releases', + }, + }, + ], + {staleTime: 60_000} + ); + + const rows: ReleaseRow[] = data?.data ?? []; + const releases: AffectedRelease[] = rows + .filter((row: ReleaseRow) => Boolean(row.release)) + .map((row: ReleaseRow) => ({ + release: row.release, + count: row['count_unique(event_id)'], + })); + + return {releases, isLoading, isError}; +} diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts new file mode 100644 index 00000000000000..d355f0ccd36077 --- /dev/null +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts @@ -0,0 +1,46 @@ +import type {Project} from 'sentry/types/project'; +import {getApiUrl} from 'sentry/utils/api/getApiUrl'; +import {useApiQuery} from 'sentry/utils/queryClient'; +import {useOrganization} from 'sentry/utils/useOrganization'; + +interface CountResult { + data: Array<{'count_unique(event_id)': number}>; +} + +export interface ImpactedEventsCount { + count: number | null; + isError: boolean; + isLoading: boolean; +} + +interface Options { + project: Project; +} + +export function useImpactedEventsCount({project}: Options): ImpactedEventsCount { + const organization = useOrganization(); + + const {data, isLoading, isError} = useApiQuery( + [ + getApiUrl('/organizations/$organizationIdOrSlug/events/', { + path: {organizationIdOrSlug: organization.slug}, + }), + { + query: { + dataset: 'processing_errors', + field: ['count_unique(event_id)'], + statsPeriod: '30d', + project: project.id, + referrer: 'api.issues.sourcemap-configuration.impact', + }, + }, + ], + {staleTime: 60_000} + ); + + return { + count: data?.data?.[0]?.['count_unique(event_id)'] ?? null, + isLoading, + isError, + }; +} diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts new file mode 100644 index 00000000000000..3b28eb69236c69 --- /dev/null +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts @@ -0,0 +1,66 @@ +import type {Project} from 'sentry/types/project'; +import {getApiUrl} from 'sentry/utils/api/getApiUrl'; +import {useApiQuery} from 'sentry/utils/queryClient'; +import {useOrganization} from 'sentry/utils/useOrganization'; + +type SampleRow = { + event_id: string; + group_id: string; + timestamp: string; + title: string; +}; + +interface SamplesResult { + data: SampleRow[]; +} + +export interface SampleEvent { + eventId: string; + groupId: string; + timestamp: string; + title: string; +} + +export interface SampleEventsResult { + events: SampleEvent[]; + isError: boolean; + isLoading: boolean; +} + +interface Options { + project: Project; +} + +export function useSampleEvents({project}: Options): SampleEventsResult { + const organization = useOrganization(); + + const {data, isLoading, isError} = useApiQuery( + [ + getApiUrl('/organizations/$organizationIdOrSlug/events/', { + path: {organizationIdOrSlug: organization.slug}, + }), + { + query: { + dataset: 'processing_errors', + field: ['title', 'event_id', 'group_id', 'timestamp'], + sort: '-timestamp', + per_page: 5, + statsPeriod: '30d', + project: project.id, + referrer: 'api.issues.sourcemap-configuration.impact-samples', + }, + }, + ], + {staleTime: 60_000} + ); + + const rows: SampleRow[] = data?.data ?? []; + const events: SampleEvent[] = rows.map((row: SampleRow) => ({ + title: row.title, + eventId: row.event_id, + groupId: row.group_id, + timestamp: row.timestamp, + })); + + return {events, isLoading, isError}; +} diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/sourceMapIssueDetails.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/sourceMapIssueDetails.tsx index a5fcd87e655137..77ba0c6a9b547e 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/sourceMapIssueDetails.tsx +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/sourceMapIssueDetails.tsx @@ -30,7 +30,7 @@ export function SourceMapIssueDetails({event, project}: SourceMapIssueDetailsPro - + ); } diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/useProcessingErrorsQuery.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/useProcessingErrorsQuery.ts deleted file mode 100644 index ca432378e2eafa..00000000000000 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/useProcessingErrorsQuery.ts +++ /dev/null @@ -1,161 +0,0 @@ -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {useApiQuery} from 'sentry/utils/queryClient'; -import {useOrganization} from 'sentry/utils/useOrganization'; - -const COUNT_FIELD = 'count_unique(event_id)' as const; -const RELEASE_FIELDS = ['release', COUNT_FIELD] as const; -const SAMPLE_FIELDS = ['title', 'event_id', 'group_id', 'timestamp'] as const; - -interface ProcessingErrorsCountResult { - data: Array>; -} - -type ReleaseRow = {release: string} & Record; - -interface ProcessingErrorsReleasesResult { - data: ReleaseRow[]; -} - -export interface ProcessingErrorsCount { - count: number | null; - isError: boolean; - isLoading: boolean; -} - -export interface AffectedRelease { - count: number; - release: string; -} - -export interface SampleEvent { - eventId: string; - groupId: string; - timestamp: string; - title: string; -} - -export interface SampleEventsResult { - events: SampleEvent[]; - isError: boolean; - isLoading: boolean; -} - -export interface AffectedReleasesResult { - isError: boolean; - isLoading: boolean; - releases: AffectedRelease[]; -} - -interface UseProcessingErrorsQueryOptions { - projectId: string; -} - -export function useProcessingErrorsQuery({ - projectId, -}: UseProcessingErrorsQueryOptions): ProcessingErrorsCount { - const organization = useOrganization(); - - const {data, isLoading, isError} = useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/events/', { - path: {organizationIdOrSlug: organization.slug}, - }), - { - query: { - dataset: 'processing_errors', - field: [COUNT_FIELD], - statsPeriod: '30d', - project: projectId, - referrer: 'api.issues.sourcemap-configuration.impact', - }, - }, - ], - {staleTime: 60_000} - ); - - return { - count: data?.data?.[0]?.[COUNT_FIELD] ?? null, - isLoading, - isError, - }; -} - -export function useAffectedReleasesQuery({ - projectId, -}: UseProcessingErrorsQueryOptions): AffectedReleasesResult { - const organization = useOrganization(); - - const {data, isLoading, isError} = useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/events/', { - path: {organizationIdOrSlug: organization.slug}, - }), - { - query: { - dataset: 'processing_errors', - field: RELEASE_FIELDS, - sort: `-${COUNT_FIELD}`, - per_page: 5, - statsPeriod: '30d', - project: projectId, - referrer: 'api.issues.sourcemap-configuration.impact-releases', - }, - }, - ], - {staleTime: 60_000} - ); - - const rows: ReleaseRow[] = data?.data ?? []; - const releases: AffectedRelease[] = rows - .filter((row: ReleaseRow) => Boolean(row.release)) - .map((row: ReleaseRow) => ({release: row.release, count: row[COUNT_FIELD]})); - - return {releases, isLoading, isError}; -} - -type SampleRow = { - event_id: string; - group_id: string; - timestamp: string; - title: string; -}; - -interface ProcessingErrorsSamplesResult { - data: SampleRow[]; -} - -export function useSampleEventsQuery({ - projectId, -}: UseProcessingErrorsQueryOptions): SampleEventsResult { - const organization = useOrganization(); - - const {data, isLoading, isError} = useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/events/', { - path: {organizationIdOrSlug: organization.slug}, - }), - { - query: { - dataset: 'processing_errors', - field: SAMPLE_FIELDS, - sort: '-timestamp', - per_page: 5, - statsPeriod: '30d', - project: projectId, - referrer: 'api.issues.sourcemap-configuration.impact-samples', - }, - }, - ], - {staleTime: 60_000} - ); - - const rows: SampleRow[] = data?.data ?? []; - const events: SampleEvent[] = rows.map((row: SampleRow) => ({ - title: row.title, - eventId: row.event_id, - groupId: row.group_id, - timestamp: row.timestamp, - })); - - return {events, isLoading, isError}; -} From 5fd967768b86089ff87d9f5c598f6132c94d8fef Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Mon, 13 Apr 2026 20:36:59 +0600 Subject: [PATCH 03/13] feat(source-map-config-issues): Iterating --- .../sourceMapIssues/impactSection.spec.tsx | 99 +++++++++++++++++++ .../queries/useImpactedEventsCount.ts | 2 +- 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.spec.tsx diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.spec.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.spec.tsx new file mode 100644 index 00000000000000..0a00edc86add27 --- /dev/null +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.spec.tsx @@ -0,0 +1,99 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ProjectFixture} from 'sentry-fixture/project'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {ImpactSection} from './impactSection'; + +const REFERRER_EVENTS_COUNT = 'api.issues.sourcemap-configuration.impact-events-count'; +const REFERRER_RELEASES = 'api.issues.sourcemap-configuration.impact-releases'; +const REFERRER_SAMPLES = 'api.issues.sourcemap-configuration.impact-samples'; + +describe('ImpactSection', () => { + const organization = OrganizationFixture(); + const project = ProjectFixture(); + const eventsUrl = `/organizations/${organization.slug}/events/`; + + beforeEach(() => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_EVENTS_COUNT})], + body: {data: [{'count_unique(event_id)': 0}]}, + }); + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_RELEASES})], + body: {data: []}, + }); + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_SAMPLES})], + body: {data: []}, + }); + }); + + it('renders the Impact heading with event count', async () => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_EVENTS_COUNT})], + body: {data: [{'count_unique(event_id)': 17}]}, + }); + + render(, {organization}); + + expect(screen.getByText('Impact')).toBeInTheDocument(); + expect( + await screen.findByText( + '17 events with unreadable stack traces in the last 30 days' + ) + ).toBeInTheDocument(); + }); + + it('renders affected releases with event counts', async () => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_RELEASES})], + body: { + data: [ + {release: 'v2.4.1', 'count_unique(event_id)': 8}, + {release: 'v2.3.0', 'count_unique(event_id)': 3}, + ], + }, + }); + + render(, {organization}); + + expect(await screen.findByText('Affected releases')).toBeInTheDocument(); + expect(screen.getByText('v2.4.1')).toBeInTheDocument(); + expect(screen.getByText('8 events')).toBeInTheDocument(); + expect(screen.getByText('v2.3.0')).toBeInTheDocument(); + expect(screen.getByText('3 events')).toBeInTheDocument(); + }); + + it('renders sample events with links to issue details', async () => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_SAMPLES})], + body: { + data: [ + { + title: 'TypeError: Cannot read property', + event_id: 'abc123', + group_id: 'group456', + timestamp: '2024-01-15T10:00:00.000Z', + }, + ], + }, + }); + + render(, {organization}); + + expect(await screen.findByText('Sample events')).toBeInTheDocument(); + + const link = screen.getByRole('link', {name: 'TypeError: Cannot read property'}); + expect(link).toHaveAttribute( + 'href', + `/organizations/${organization.slug}/issues/group456/events/abc123/` + ); + }); +}); diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts index d355f0ccd36077..27840fd916e501 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts @@ -31,7 +31,7 @@ export function useImpactedEventsCount({project}: Options): ImpactedEventsCount field: ['count_unique(event_id)'], statsPeriod: '30d', project: project.id, - referrer: 'api.issues.sourcemap-configuration.impact', + referrer: 'api.issues.sourcemap-configuration.impact-events-count', }, }, ], From b7bb00743d83089814b7095637568a8612a965a2 Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Mon, 13 Apr 2026 20:45:07 +0600 Subject: [PATCH 04/13] feat(source-map-config-issues): Fixing CI issues --- .../sourceMapIssues/queries/useAffectedReleases.ts | 8 ++++---- .../sourceMapIssues/queries/useImpactedEventsCount.ts | 2 +- .../sourceMapIssues/queries/useSampleEvents.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts index b231c1ecc8faf5..1d6dfbb916f56d 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts @@ -9,12 +9,12 @@ interface ReleasesResult { data: ReleaseRow[]; } -export interface AffectedRelease { +interface AffectedRelease { count: number; release: string; } -export interface AffectedReleasesResult { +interface AffectedReleasesResult { isError: boolean; isLoading: boolean; releases: AffectedRelease[]; @@ -47,8 +47,8 @@ export function useAffectedReleases({project}: Options): AffectedReleasesResult {staleTime: 60_000} ); - const rows: ReleaseRow[] = data?.data ?? []; - const releases: AffectedRelease[] = rows + const rows = data?.data ?? []; + const releases = rows .filter((row: ReleaseRow) => Boolean(row.release)) .map((row: ReleaseRow) => ({ release: row.release, diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts index 27840fd916e501..43e40370f7327f 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts @@ -7,7 +7,7 @@ interface CountResult { data: Array<{'count_unique(event_id)': number}>; } -export interface ImpactedEventsCount { +interface ImpactedEventsCount { count: number | null; isError: boolean; isLoading: boolean; diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts index 3b28eb69236c69..e94c055c8f33d2 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts @@ -14,14 +14,14 @@ interface SamplesResult { data: SampleRow[]; } -export interface SampleEvent { +interface SampleEvent { eventId: string; groupId: string; timestamp: string; title: string; } -export interface SampleEventsResult { +interface SampleEventsResult { events: SampleEvent[]; isError: boolean; isLoading: boolean; @@ -54,8 +54,8 @@ export function useSampleEvents({project}: Options): SampleEventsResult { {staleTime: 60_000} ); - const rows: SampleRow[] = data?.data ?? []; - const events: SampleEvent[] = rows.map((row: SampleRow) => ({ + const rows = data?.data ?? []; + const events = rows.map((row: SampleRow) => ({ title: row.title, eventId: row.event_id, groupId: row.group_id, From e2ce16837e62df63f385e9f4ffdb80ef715cbf8b Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Mon, 13 Apr 2026 22:30:39 +0600 Subject: [PATCH 05/13] feat(source-map-config-issues): Adding Sentry Configuration nav item under issues --- static/app/router/routes.tsx | 4 +++ .../issueList/pages/sentryConfiguration.tsx | 27 ++++++++++++++ static/app/views/issueList/taxonomies.tsx | 9 +++++ .../issues/issuesSecondaryNavigation.tsx | 35 ++++++++++++------- 4 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 static/app/views/issueList/pages/sentryConfiguration.tsx diff --git a/static/app/router/routes.tsx b/static/app/router/routes.tsx index ac059dae417da5..19b8680cc4e73d 100644 --- a/static/app/router/routes.tsx +++ b/static/app/router/routes.tsx @@ -2522,6 +2522,10 @@ function buildRoutes(): RouteObject[] { path: `${IssueTaxonomy.WARNINGS}/`, component: make(() => import('sentry/views/issueList/pages/warnings')), }, + { + path: `${IssueTaxonomy.SENTRY_CONFIGURATION}/`, + component: make(() => import('sentry/views/issueList/pages/sentryConfiguration')), + }, { path: 'instrumentation/', component: make(() => import('sentry/views/issueList/pages/instrumentation')), diff --git a/static/app/views/issueList/pages/sentryConfiguration.tsx b/static/app/views/issueList/pages/sentryConfiguration.tsx new file mode 100644 index 00000000000000..8846ca931be49f --- /dev/null +++ b/static/app/views/issueList/pages/sentryConfiguration.tsx @@ -0,0 +1,27 @@ +import {NoProjectMessage} from 'sentry/components/noProjectMessage'; +import {PageFiltersContainer} from 'sentry/components/pageFilters/container'; +import {useOrganization} from 'sentry/utils/useOrganization'; +import {IssueListContainer} from 'sentry/views/issueList'; +import IssueListOverview from 'sentry/views/issueList/overview'; +import {ISSUE_TAXONOMY_CONFIG, IssueTaxonomy} from 'sentry/views/issueList/taxonomies'; + +const CONFIG = ISSUE_TAXONOMY_CONFIG[IssueTaxonomy.SENTRY_CONFIGURATION]; +const QUERY = `is:unresolved issue.category:[${CONFIG.categories.join(',')}]`; + +export default function SentryConfigurationPage() { + const organization = useOrganization(); + + return ( + + + + + + + + ); +} diff --git a/static/app/views/issueList/taxonomies.tsx b/static/app/views/issueList/taxonomies.tsx index c592368908772e..3d5b50f88f9a7e 100644 --- a/static/app/views/issueList/taxonomies.tsx +++ b/static/app/views/issueList/taxonomies.tsx @@ -7,6 +7,7 @@ export enum IssueTaxonomy { ERRORS_AND_OUTAGES = 'errors-outages', BREACHED_METRICS = 'breached-metrics', WARNINGS = 'warnings', + SENTRY_CONFIGURATION = 'sentry-configuration', } export const ISSUE_TAXONOMY_CONFIG: Record< @@ -47,4 +48,12 @@ export const ISSUE_TAXONOMY_CONFIG: Record< 'Issues in your code or configuration that may not break functionality but can degrade performance or user experience' ), }, + [IssueTaxonomy.SENTRY_CONFIGURATION]: { + categories: [IssueCategory.CONFIGURATION], + label: t('Sentry Configuration'), + key: 'sentry-configuration', + description: t( + 'Issues detected from SDK or tooling configuration problems, such as missing or broken source maps.' + ), + }, }; diff --git a/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx b/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx index 7686e863704d6d..14e18e9e1a9a79 100644 --- a/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx +++ b/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx @@ -10,7 +10,7 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {t, tct} from 'sentry/locale'; import {useOrganization} from 'sentry/utils/useOrganization'; import {makeAutomationBasePathname} from 'sentry/views/automations/pathnames'; -import {ISSUE_TAXONOMY_CONFIG} from 'sentry/views/issueList/taxonomies'; +import {ISSUE_TAXONOMY_CONFIG, IssueTaxonomy} from 'sentry/views/issueList/taxonomies'; import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext'; import {SecondaryNavigation} from 'sentry/views/navigation/secondary/components'; import {IssueViews} from 'sentry/views/navigation/secondary/sections/issues/issueViews/issueViews'; @@ -38,17 +38,19 @@ export function IssuesSecondaryNavigation() { - {Object.values(ISSUE_TAXONOMY_CONFIG).map(({key, label}) => ( - - - {label} - - - ))} + {Object.values(ISSUE_TAXONOMY_CONFIG) + .filter(({key}) => key !== IssueTaxonomy.SENTRY_CONFIGURATION) + .map(({key, label}) => ( + + + {label} + + + ))} + + + {ISSUE_TAXONOMY_CONFIG[IssueTaxonomy.SENTRY_CONFIGURATION].label} + + {organization.features.includes('seer-autopilot') && ( Date: Mon, 13 Apr 2026 22:49:50 +0600 Subject: [PATCH 06/13] feat(source-map-config-issues): Adding Sentry Configuration nav item under issues --- static/app/views/issueList/taxonomies.tsx | 2 +- .../issues/issuesSecondaryNavigation.tsx | 35 +++++++------------ 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/static/app/views/issueList/taxonomies.tsx b/static/app/views/issueList/taxonomies.tsx index 3d5b50f88f9a7e..6d322109357b93 100644 --- a/static/app/views/issueList/taxonomies.tsx +++ b/static/app/views/issueList/taxonomies.tsx @@ -53,7 +53,7 @@ export const ISSUE_TAXONOMY_CONFIG: Record< label: t('Sentry Configuration'), key: 'sentry-configuration', description: t( - 'Issues detected from SDK or tooling configuration problems, such as missing or broken source maps.' + 'Issues detected from SDK or tooling configuration problems that degrade your ability to debug telemetry using Sentry.' ), }, }; diff --git a/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx b/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx index 14e18e9e1a9a79..7686e863704d6d 100644 --- a/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx +++ b/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx @@ -10,7 +10,7 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {t, tct} from 'sentry/locale'; import {useOrganization} from 'sentry/utils/useOrganization'; import {makeAutomationBasePathname} from 'sentry/views/automations/pathnames'; -import {ISSUE_TAXONOMY_CONFIG, IssueTaxonomy} from 'sentry/views/issueList/taxonomies'; +import {ISSUE_TAXONOMY_CONFIG} from 'sentry/views/issueList/taxonomies'; import {usePrimaryNavigation} from 'sentry/views/navigation/primaryNavigationContext'; import {SecondaryNavigation} from 'sentry/views/navigation/secondary/components'; import {IssueViews} from 'sentry/views/navigation/secondary/sections/issues/issueViews/issueViews'; @@ -38,19 +38,17 @@ export function IssuesSecondaryNavigation() { - {Object.values(ISSUE_TAXONOMY_CONFIG) - .filter(({key}) => key !== IssueTaxonomy.SENTRY_CONFIGURATION) - .map(({key, label}) => ( - - - {label} - - - ))} + {Object.values(ISSUE_TAXONOMY_CONFIG).map(({key, label}) => ( + + + {label} + + + ))} - - - {ISSUE_TAXONOMY_CONFIG[IssueTaxonomy.SENTRY_CONFIGURATION].label} - - {organization.features.includes('seer-autopilot') && ( Date: Mon, 13 Apr 2026 23:19:01 +0600 Subject: [PATCH 07/13] feat(source-map-config-issues): Adding Sentry Configuration nav item under issues --- .../sourceMapIssues/impactSection.tsx | 104 ++++++++++-------- .../queries/useSampleEvents.ts | 4 +- 2 files changed, 62 insertions(+), 46 deletions(-) diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx index ce8510d1755554..0763630277624b 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx @@ -27,8 +27,8 @@ function EventsCount({project}: {project: Project}) { if (isError) { return ; } - if (count === null) { - return null; + if (!count) { + return {t('No impacted events found in the last 30 days.')}; } return ( @@ -44,29 +44,35 @@ function EventsCount({project}: {project: Project}) { function AffectedReleases({project}: {project: Project}) { const {releases, isLoading, isError} = useAffectedReleases({project}); - if (isLoading) { - return ; - } - if (isError) { - return ; - } - if (releases.length === 0) { - return null; + function renderContent() { + if (isLoading) { + return ; + } + if (isError) { + return ; + } + if (releases.length === 0) { + return {t('No affected releases found in the last 30 days.')}; + } + return ( + + {releases.map(({release, count}) => ( + + + · + {tn('%s event', '%s events', count)} + + ))} + + ); } + return ( - {t('Affected releases')} - - {releases.map(({release, count}) => ( - - - · - {tn('%s event', '%s events', count)} - - ))} - + {t('Affected releases')} + {renderContent()} ); @@ -76,23 +82,21 @@ function SampleEvents({project}: {project: Project}) { const organization = useOrganization(); const {events, isLoading, isError} = useSampleEvents({project}); - if (isLoading) { - return ; - } - if (isError) { - return ; - } - if (events.length === 0) { - return null; - } - return ( - - - - {t('Sample events')} - - {events.map(({eventId, groupId, title, timestamp}) => ( - + function renderContent() { + if (isLoading) { + return ; + } + if (isError) { + return ; + } + if (events.length === 0) { + return {t('No sample events found in the last 30 days.')}; + } + return ( + + {events.map(({eventId, groupId, title, timestamp}) => ( + + {groupId ? ( {title} - · - - - - - ))} - + ) : ( + {title} + )} + · + + + + + ))} + + ); + } + + return ( + + + + {t('Sample events')} + {renderContent()} ); diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts index e94c055c8f33d2..0b43872b52ba80 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts @@ -5,7 +5,7 @@ import {useOrganization} from 'sentry/utils/useOrganization'; type SampleRow = { event_id: string; - group_id: string; + group_id: string | null; timestamp: string; title: string; }; @@ -16,7 +16,7 @@ interface SamplesResult { interface SampleEvent { eventId: string; - groupId: string; + groupId: string | null; timestamp: string; title: string; } From f5d95858fdf455e119d3c4672621dd11af085ed0 Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Tue, 14 Apr 2026 17:49:47 +0600 Subject: [PATCH 08/13] feat(source-map-config-issues): Gating by feature flag --- static/app/views/issueList/taxonomies.tsx | 2 ++ .../issues/issuesSecondaryNavigation.tsx | 27 +++++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/static/app/views/issueList/taxonomies.tsx b/static/app/views/issueList/taxonomies.tsx index 6d322109357b93..7fbc0dd65a6d0c 100644 --- a/static/app/views/issueList/taxonomies.tsx +++ b/static/app/views/issueList/taxonomies.tsx @@ -17,6 +17,7 @@ export const ISSUE_TAXONOMY_CONFIG: Record< description: ReactNode; key: string; label: string; + featureFlag?: string; } > = { [IssueTaxonomy.ERRORS_AND_OUTAGES]: { @@ -55,5 +56,6 @@ export const ISSUE_TAXONOMY_CONFIG: Record< description: t( 'Issues detected from SDK or tooling configuration problems that degrade your ability to debug telemetry using Sentry.' ), + featureFlag: 'issue-sourcemap-configuration-visible', }, }; diff --git a/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx b/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx index 7686e863704d6d..f68973ef8641e4 100644 --- a/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx +++ b/static/app/views/navigation/secondary/sections/issues/issuesSecondaryNavigation.tsx @@ -38,17 +38,22 @@ export function IssuesSecondaryNavigation() { - {Object.values(ISSUE_TAXONOMY_CONFIG).map(({key, label}) => ( - - - {label} - - - ))} + {Object.values(ISSUE_TAXONOMY_CONFIG) + .filter( + ({featureFlag}) => + !featureFlag || organization.features.includes(featureFlag) + ) + .map(({key, label}) => ( + + + {label} + + + ))} Date: Tue, 14 Apr 2026 21:58:29 +0600 Subject: [PATCH 09/13] feat(source-map-config-issues): Fixing tests --- .../sourceMapIssues/impactSection.spec.tsx | 194 +++++++++++++----- .../sourceMapIssues/impactSection.tsx | 10 +- .../queries/useAffectedReleases.ts | 46 ++--- .../queries/useImpactedEventsCount.ts | 31 ++- .../queries/useSampleEvents.ts | 56 ++--- 5 files changed, 204 insertions(+), 133 deletions(-) diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.spec.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.spec.tsx index 0a00edc86add27..28e63ed0af39ce 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.spec.tsx +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.spec.tsx @@ -32,68 +32,164 @@ describe('ImpactSection', () => { }); }); - it('renders the Impact heading with event count', async () => { - MockApiClient.addMockResponse({ - url: eventsUrl, - match: [MockApiClient.matchQuery({referrer: REFERRER_EVENTS_COUNT})], - body: {data: [{'count_unique(event_id)': 17}]}, + describe('EventsCount', () => { + it('renders the event count', async () => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_EVENTS_COUNT})], + body: {data: [{'count_unique(event_id)': 17}]}, + }); + + render(, {organization}); + + expect(screen.getByText('Impact')).toBeInTheDocument(); + expect( + await screen.findByText( + '17 events with unreadable stack traces in the last 30 days' + ) + ).toBeInTheDocument(); }); - render(, {organization}); + it('renders empty state when count is zero', async () => { + render(, {organization}); + + expect( + await screen.findByText('No impacted events found in the last 30 days.') + ).toBeInTheDocument(); + }); + + it('renders error state when the request fails', async () => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_EVENTS_COUNT})], + statusCode: 500, + body: {}, + }); + + render(, {organization}); - expect(screen.getByText('Impact')).toBeInTheDocument(); - expect( - await screen.findByText( - '17 events with unreadable stack traces in the last 30 days' - ) - ).toBeInTheDocument(); + expect( + await screen.findByText('Unable to load impacted events count.') + ).toBeInTheDocument(); + }); }); - it('renders affected releases with event counts', async () => { - MockApiClient.addMockResponse({ - url: eventsUrl, - match: [MockApiClient.matchQuery({referrer: REFERRER_RELEASES})], - body: { - data: [ - {release: 'v2.4.1', 'count_unique(event_id)': 8}, - {release: 'v2.3.0', 'count_unique(event_id)': 3}, - ], - }, + describe('AffectedReleases', () => { + it('renders releases with event counts', async () => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_RELEASES})], + body: { + data: [ + {release: 'v2.4.1', 'count_unique(event_id)': 8}, + {release: 'v2.3.0', 'count_unique(event_id)': 3}, + ], + }, + }); + + render(, {organization}); + + expect(await screen.findByText('v2.4.1')).toBeInTheDocument(); + expect(screen.getByText('8 events')).toBeInTheDocument(); + expect(screen.getByText('v2.3.0')).toBeInTheDocument(); + expect(screen.getByText('3 events')).toBeInTheDocument(); }); - render(, {organization}); + it('renders empty state when there are no releases', async () => { + render(, {organization}); + + expect( + await screen.findByText('No affected releases found in the last 30 days.') + ).toBeInTheDocument(); + }); - expect(await screen.findByText('Affected releases')).toBeInTheDocument(); - expect(screen.getByText('v2.4.1')).toBeInTheDocument(); - expect(screen.getByText('8 events')).toBeInTheDocument(); - expect(screen.getByText('v2.3.0')).toBeInTheDocument(); - expect(screen.getByText('3 events')).toBeInTheDocument(); + it('renders error state when the request fails', async () => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_RELEASES})], + statusCode: 500, + body: {}, + }); + + render(, {organization}); + + expect( + await screen.findByText('Unable to load affected releases.') + ).toBeInTheDocument(); + }); }); - it('renders sample events with links to issue details', async () => { - MockApiClient.addMockResponse({ - url: eventsUrl, - match: [MockApiClient.matchQuery({referrer: REFERRER_SAMPLES})], - body: { - data: [ - { - title: 'TypeError: Cannot read property', - event_id: 'abc123', - group_id: 'group456', - timestamp: '2024-01-15T10:00:00.000Z', - }, - ], - }, + describe('SampleEvents', () => { + it('renders sample events with links to issue details', async () => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_SAMPLES})], + body: { + data: [ + { + title: 'TypeError: Cannot read property', + event_id: 'abc123', + group_id: 'group456', + timestamp: '2024-01-15T10:00:00.000Z', + }, + ], + }, + }); + + render(, {organization}); + + const link = await screen.findByText('TypeError: Cannot read property'); + expect(link).toHaveAttribute( + 'href', + `/organizations/${organization.slug}/issues/group456/events/abc123/` + ); }); - render(, {organization}); + it('renders title as muted text when group_id is null', async () => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_SAMPLES})], + body: { + data: [ + { + title: 'TypeError: Cannot read property', + event_id: 'abc123', + group_id: null, + timestamp: '2024-01-15T10:00:00.000Z', + }, + ], + }, + }); - expect(await screen.findByText('Sample events')).toBeInTheDocument(); + render(, {organization}); - const link = screen.getByRole('link', {name: 'TypeError: Cannot read property'}); - expect(link).toHaveAttribute( - 'href', - `/organizations/${organization.slug}/issues/group456/events/abc123/` - ); + await screen.findByText('TypeError: Cannot read property'); + expect( + screen.queryByRole('link', {name: 'TypeError: Cannot read property'}) + ).not.toBeInTheDocument(); + }); + + it('renders empty state when there are no sample events', async () => { + render(, {organization}); + + expect( + await screen.findByText('No sample events found in the last 30 days.') + ).toBeInTheDocument(); + }); + + it('renders error state when the request fails', async () => { + MockApiClient.addMockResponse({ + url: eventsUrl, + match: [MockApiClient.matchQuery({referrer: REFERRER_SAMPLES})], + statusCode: 500, + body: {}, + }); + + render(, {organization}); + + expect( + await screen.findByText('Unable to load sample events.') + ).toBeInTheDocument(); + }); }); }); diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx index 0763630277624b..d55f39e68a3cff 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/impactSection.tsx @@ -25,7 +25,7 @@ function EventsCount({project}: {project: Project}) { return ; } if (isError) { - return ; + return ; } if (!count) { return {t('No impacted events found in the last 30 days.')}; @@ -94,12 +94,12 @@ function SampleEvents({project}: {project: Project}) { } return ( - {events.map(({eventId, groupId, title, timestamp}) => ( - - {groupId ? ( + {events.map(({event_id, group_id, title, timestamp}) => ( + + {group_id ? ( {title} diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts index 1d6dfbb916f56d..d1620509d72d49 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useAffectedReleases.ts @@ -1,6 +1,7 @@ +import {useQuery} from '@tanstack/react-query'; + import type {Project} from 'sentry/types/project'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {useApiQuery} from 'sentry/utils/queryClient'; +import {apiOptions} from 'sentry/utils/api/apiOptions'; import {useOrganization} from 'sentry/utils/useOrganization'; type ReleaseRow = {release: string} & {'count_unique(event_id)': number}; @@ -27,33 +28,28 @@ interface Options { export function useAffectedReleases({project}: Options): AffectedReleasesResult { const organization = useOrganization(); - const {data, isLoading, isError} = useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/events/', { - path: {organizationIdOrSlug: organization.slug}, - }), - { - query: { - dataset: 'processing_errors', - field: ['release', 'count_unique(event_id)'], - sort: '-count_unique(event_id)', - per_page: 5, - statsPeriod: '30d', - project: project.id, - referrer: 'api.issues.sourcemap-configuration.impact-releases', - }, + const {data, isLoading, isError} = useQuery( + apiOptions.as()('/organizations/$organizationIdOrSlug/events/', { + path: {organizationIdOrSlug: organization.slug}, + query: { + dataset: 'processing_errors', + field: ['release', 'count_unique(event_id)'], + query: 'has:release', + sort: '-count_unique(event_id)', + per_page: 5, + statsPeriod: '30d', + project: project.id, + referrer: 'api.issues.sourcemap-configuration.impact-releases', }, - ], - {staleTime: 60_000} + staleTime: 60_000, + }) ); const rows = data?.data ?? []; - const releases = rows - .filter((row: ReleaseRow) => Boolean(row.release)) - .map((row: ReleaseRow) => ({ - release: row.release, - count: row['count_unique(event_id)'], - })); + const releases = rows.map((row: ReleaseRow) => ({ + release: row.release, + count: row['count_unique(event_id)'], + })); return {releases, isLoading, isError}; } diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts index 43e40370f7327f..a2bb4e50b6ac99 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useImpactedEventsCount.ts @@ -1,6 +1,7 @@ +import {useQuery} from '@tanstack/react-query'; + import type {Project} from 'sentry/types/project'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {useApiQuery} from 'sentry/utils/queryClient'; +import {apiOptions} from 'sentry/utils/api/apiOptions'; import {useOrganization} from 'sentry/utils/useOrganization'; interface CountResult { @@ -20,22 +21,18 @@ interface Options { export function useImpactedEventsCount({project}: Options): ImpactedEventsCount { const organization = useOrganization(); - const {data, isLoading, isError} = useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/events/', { - path: {organizationIdOrSlug: organization.slug}, - }), - { - query: { - dataset: 'processing_errors', - field: ['count_unique(event_id)'], - statsPeriod: '30d', - project: project.id, - referrer: 'api.issues.sourcemap-configuration.impact-events-count', - }, + const {data, isLoading, isError} = useQuery( + apiOptions.as()('/organizations/$organizationIdOrSlug/events/', { + path: {organizationIdOrSlug: organization.slug}, + query: { + dataset: 'processing_errors', + field: ['count_unique(event_id)'], + statsPeriod: '30d', + project: project.id, + referrer: 'api.issues.sourcemap-configuration.impact-events-count', }, - ], - {staleTime: 60_000} + staleTime: 60_000, + }) ); return { diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts index 0b43872b52ba80..ca07238779a7d9 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts @@ -1,9 +1,10 @@ +import {useQuery} from '@tanstack/react-query'; + import type {Project} from 'sentry/types/project'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {useApiQuery} from 'sentry/utils/queryClient'; +import {apiOptions} from 'sentry/utils/api/apiOptions'; import {useOrganization} from 'sentry/utils/useOrganization'; -type SampleRow = { +export type SampleEvent = { event_id: string; group_id: string | null; timestamp: string; @@ -11,14 +12,7 @@ type SampleRow = { }; interface SamplesResult { - data: SampleRow[]; -} - -interface SampleEvent { - eventId: string; - groupId: string | null; - timestamp: string; - title: string; + data: SampleEvent[]; } interface SampleEventsResult { @@ -34,33 +28,21 @@ interface Options { export function useSampleEvents({project}: Options): SampleEventsResult { const organization = useOrganization(); - const {data, isLoading, isError} = useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/events/', { - path: {organizationIdOrSlug: organization.slug}, - }), - { - query: { - dataset: 'processing_errors', - field: ['title', 'event_id', 'group_id', 'timestamp'], - sort: '-timestamp', - per_page: 5, - statsPeriod: '30d', - project: project.id, - referrer: 'api.issues.sourcemap-configuration.impact-samples', - }, + const {data, isLoading, isError} = useQuery( + apiOptions.as()('/organizations/$organizationIdOrSlug/events/', { + path: {organizationIdOrSlug: organization.slug}, + query: { + dataset: 'processing_errors', + field: ['title', 'event_id', 'group_id', 'timestamp'], + sort: '-timestamp', + per_page: 5, + statsPeriod: '30d', + project: project.id, + referrer: 'api.issues.sourcemap-configuration.impact-samples', }, - ], - {staleTime: 60_000} + staleTime: 60_000, + }) ); - const rows = data?.data ?? []; - const events = rows.map((row: SampleRow) => ({ - title: row.title, - eventId: row.event_id, - groupId: row.group_id, - timestamp: row.timestamp, - })); - - return {events, isLoading, isError}; + return {events: data?.data ?? [], isLoading, isError}; } From 391b2173c47dfc3ac5e25455178fd1a40bdab37d Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Tue, 14 Apr 2026 22:02:32 +0600 Subject: [PATCH 10/13] feat(source-map-config-issues): Fixing tests --- .../sourceMapIssues/queries/useSampleEvents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts index ca07238779a7d9..9c3c5ac51d430a 100644 --- a/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts +++ b/static/app/views/issueDetails/configurationIssues/sourceMapIssues/queries/useSampleEvents.ts @@ -4,7 +4,7 @@ import type {Project} from 'sentry/types/project'; import {apiOptions} from 'sentry/utils/api/apiOptions'; import {useOrganization} from 'sentry/utils/useOrganization'; -export type SampleEvent = { +type SampleEvent = { event_id: string; group_id: string | null; timestamp: string; From 751c17b017d7e9406e0d579d69311f92d0adb052 Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Tue, 14 Apr 2026 22:06:33 +0600 Subject: [PATCH 11/13] feat(source-map-config-issues): Gating by feature flag --- .../issueList/pages/sentryConfiguration.tsx | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/static/app/views/issueList/pages/sentryConfiguration.tsx b/static/app/views/issueList/pages/sentryConfiguration.tsx index 8846ca931be49f..41e9c87b69e8cd 100644 --- a/static/app/views/issueList/pages/sentryConfiguration.tsx +++ b/static/app/views/issueList/pages/sentryConfiguration.tsx @@ -1,3 +1,4 @@ +import Feature from 'sentry/components/acl/feature'; import {NoProjectMessage} from 'sentry/components/noProjectMessage'; import {PageFiltersContainer} from 'sentry/components/pageFilters/container'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -12,16 +13,18 @@ export default function SentryConfigurationPage() { const organization = useOrganization(); return ( - - - - - - - + + + + + + + + + ); } From 9235abf02ca5b02260a89ba0ed20aa527ba04e48 Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Tue, 14 Apr 2026 22:19:39 +0600 Subject: [PATCH 12/13] feat(source-map-config-issues): Addressing PR suggestions --- scripts/routes.ts | 1 + .../ui/commandPaletteGlobalActions.tsx | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/scripts/routes.ts b/scripts/routes.ts index 7ed56c4a0bae9c..23af4a3b9343bf 100644 --- a/scripts/routes.ts +++ b/scripts/routes.ts @@ -207,6 +207,7 @@ const CONSTANTS: Record = { 'IssueTaxonomy.ERRORS_AND_OUTAGES': 'errors-outages', 'IssueTaxonomy.BREACHED_METRICS': 'breached-metrics', 'IssueTaxonomy.WARNINGS': 'warnings', + 'IssueTaxonomy.SENTRY_CONFIGURATION': 'sentry-configuration', }; function resolveTemplate(expr: string): string { diff --git a/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx b/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx index e84c1d1d3a9ceb..bec07c5afaca05 100644 --- a/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx +++ b/static/app/components/commandPalette/ui/commandPaletteGlobalActions.tsx @@ -97,13 +97,18 @@ export function GlobalCommandPaletteActions() { }}> - {Object.values(ISSUE_TAXONOMY_CONFIG).map(config => ( - - ))} + {Object.values(ISSUE_TAXONOMY_CONFIG) + .filter( + ({featureFlag}) => + !featureFlag || organization.features.includes(featureFlag) + ) + .map(config => ( + + ))} Date: Tue, 14 Apr 2026 22:27:55 +0600 Subject: [PATCH 13/13] feat(source-map-config-issues): Addressing PR suggestions --- static/app/views/issueList/pages/sentryConfiguration.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/issueList/pages/sentryConfiguration.tsx b/static/app/views/issueList/pages/sentryConfiguration.tsx index 41e9c87b69e8cd..efb2ebb7384c62 100644 --- a/static/app/views/issueList/pages/sentryConfiguration.tsx +++ b/static/app/views/issueList/pages/sentryConfiguration.tsx @@ -13,7 +13,7 @@ export default function SentryConfigurationPage() { const organization = useOrganization(); return ( - +