diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx index c437c1de6a4b77..9d5c3944efe7fc 100644 --- a/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx +++ b/static/app/views/issueDetails/groupReplays/groupReplays.spec.tsx @@ -1,9 +1,11 @@ import {duration} from 'moment-timezone'; +import {EventsStatsFixture} from 'sentry-fixture/events'; import {GroupFixture} from 'sentry-fixture/group'; import {ProjectFixture} from 'sentry-fixture/project'; import {RRWebInitFrameEventsFixture} from 'sentry-fixture/replay/rrweb'; import {ReplayListFixture} from 'sentry-fixture/replayList'; import {ReplayRecordFixture} from 'sentry-fixture/replayRecord'; +import {TagsFixture} from 'sentry-fixture/tags'; import {initializeOrg} from 'sentry-test/initializeOrg'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; @@ -110,6 +112,15 @@ describe('GroupReplays', () => { url: `/organizations/org-slug/issues/${mockGroup.id}/`, body: mockGroup, }); + MockApiClient.addMockResponse({ + url: `/organizations/org-slug/issues/${mockGroup.id}/tags/`, + body: TagsFixture(), + method: 'GET', + }); + MockApiClient.addMockResponse({ + url: `/organizations/org-slug/events-stats/`, + body: {'count()': EventsStatsFixture()}, + }); }); afterEach(() => { resetMockDate(); diff --git a/static/app/views/issueDetails/groupReplays/groupReplays.tsx b/static/app/views/issueDetails/groupReplays/groupReplays.tsx index a5321df37b74ac..a5617a5d95e6ea 100644 --- a/static/app/views/issueDetails/groupReplays/groupReplays.tsx +++ b/static/app/views/issueDetails/groupReplays/groupReplays.tsx @@ -15,12 +15,16 @@ import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; import {browserHistory} from 'sentry/utils/browserHistory'; import type EventView from 'sentry/utils/discover/eventView'; -import useReplayCountForIssues from 'sentry/utils/replayCount/useReplayCountForIssues'; import useReplayList from 'sentry/utils/replays/hooks/useReplayList'; import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import useUrlParams from 'sentry/utils/useUrlParams'; +import {useIssueDetailsEventCount} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsEventCount'; +import { + type ReplayCount, + useIssueDetailsReplayCount, +} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsReplayCount'; import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils'; import useAllMobileProj from 'sentry/views/replays/detail/useAllMobileProj'; import ReplayTable from 'sentry/views/replays/replayTable'; @@ -177,9 +181,6 @@ function GroupReplaysTable({ }) { const location = useLocation(); const urlParams = useUrlParams(); - const {getReplayCountForIssue} = useReplayCountForIssues({ - statsPeriod: '90d', - }); const hasStreamlinedUI = useHasStreamlinedUI(); const replayListData = useReplayList({ @@ -206,10 +207,13 @@ function GroupReplaysTable({ }, [location] ); + const eventCount = useIssueDetailsEventCount({group}); const selectedReplay = replays?.[selectedReplayIndex]; + const {data: replayData} = useIssueDetailsReplayCount({group}); + + const replayCount = replayData?.[group.id] ?? 0; - const replayCount = getReplayCountForIssue(group.id, group.issueCategory); const nextReplay = replays?.[selectedReplayIndex + 1]; const nextReplayText = nextReplay?.id ? `${nextReplay.user.display_name || t('Anonymous User')}` @@ -275,12 +279,12 @@ function GroupReplaysTable({ ? tn( 'There are 50+ replays for this issue across %s event', 'There are 50+ replays for this issue across %s events', - group.count + eventCount ) : t( 'There %s for this issue across %s.', tn('is %s replay', 'are %s replays', replayCount ?? 0), - tn('%s event', '%s events', group.count) + tn('%s event', '%s events', eventCount) )} {inner} diff --git a/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.tsx b/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.tsx index 5ef25bdf4a1162..bbc5991ecd5ff3 100644 --- a/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.tsx +++ b/static/app/views/issueDetails/groupReplays/useReplaysFromIssue.tsx @@ -10,6 +10,9 @@ import {decodeScalar} from 'sentry/utils/queryString'; import {DEFAULT_SORT} from 'sentry/utils/replays/fetchReplayList'; import useApi from 'sentry/utils/useApi'; import useCleanQueryParamsOnRouteLeave from 'sentry/utils/useCleanQueryParamsOnRouteLeave'; +import {useEventQuery} from 'sentry/views/issueDetails/streamline/eventSearch'; +import {useIssueDetailsEventView} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery'; +import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils'; import {REPLAY_LIST_FIELDS} from 'sentry/views/replays/types'; export default function useReplaysFromIssue({ @@ -22,6 +25,7 @@ export default function useReplaysFromIssue({ organization: Organization; }) { const api = useApi(); + const hasStreamlinedUI = useHasStreamlinedUI(); const [replayIds, setReplayIds] = useState(); @@ -53,6 +57,19 @@ export default function useReplaysFromIssue({ } }, [api, organization.slug, group.id, dataSource, location.query.environment]); + const searchQuery = useEventQuery({group}); + const replayQuery = replayIds?.length ? `id:[${String(replayIds)}]` : ''; + const combinedQuery = [searchQuery, replayQuery].join(' ').trim(); + const issueDetailsEventView = useIssueDetailsEventView({ + group, + queryProps: { + version: 2, + fields: REPLAY_LIST_FIELDS, + orderby: decodeScalar(location.query.sort, DEFAULT_SORT), + }, + }); + issueDetailsEventView.query = combinedQuery; + const eventView = useMemo(() => { if (!replayIds || !replayIds.length) { return null; @@ -78,7 +95,7 @@ export default function useReplaysFromIssue({ }, [fetchReplayIds]); return { - eventView, + eventView: hasStreamlinedUI ? issueDetailsEventView : eventView, fetchError, isFetching: replayIds === undefined, pageLinks: null, diff --git a/static/app/views/issueDetails/streamline/eventNavigation.tsx b/static/app/views/issueDetails/streamline/eventNavigation.tsx index 4997a8ebde48c2..03d6505e329936 100644 --- a/static/app/views/issueDetails/streamline/eventNavigation.tsx +++ b/static/app/views/issueDetails/streamline/eventNavigation.tsx @@ -20,7 +20,6 @@ import {SavedQueryDatasets} from 'sentry/utils/discover/types'; import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig'; import parseLinkHeader from 'sentry/utils/parseLinkHeader'; import {keepPreviousData, useApiQuery} from 'sentry/utils/queryClient'; -import useReplayCountForIssues from 'sentry/utils/replayCount/useReplayCountForIssues'; import normalizeUrl from 'sentry/utils/url/normalizeUrl'; import {useLocation} from 'sentry/utils/useLocation'; import useMedia from 'sentry/utils/useMedia'; @@ -29,6 +28,11 @@ import {useParams} from 'sentry/utils/useParams'; import {hasDatasetSelector} from 'sentry/views/dashboards/utils'; import {useGroupEventAttachments} from 'sentry/views/issueDetails/groupEventAttachments/useGroupEventAttachments'; import {useIssueDetailsEventView} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery'; +import {useIssueDetailsEventCount} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsEventCount'; +import { + type ReplayCount, + useIssueDetailsReplayCount, +} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsReplayCount'; import {Tab, TabPaths} from 'sentry/views/issueDetails/types'; import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute'; import { @@ -136,11 +140,10 @@ export function IssueEventNavigation({event, group, query}: IssueEventNavigation notifyOnChangeProps: [], } ); + const eventCount = useIssueDetailsEventCount({group}); - const {getReplayCountForIssue} = useReplayCountForIssues({ - statsPeriod: '90d', - }); - const replaysCount = getReplayCountForIssue(group.id, group.issueCategory) ?? 0; + const {data: replayData} = useIssueDetailsReplayCount({group}); + const replaysCount = replayData?.[group.id] ?? 0; const attachments = useGroupEventAttachments({ group, @@ -207,7 +210,7 @@ export function IssueEventNavigation({event, group, query}: IssueEventNavigation key: Tab.DETAILS, label: ( - {TabName[Tab.DETAILS]} + {TabName[Tab.DETAILS]} ), textValue: TabName[Tab.DETAILS], @@ -241,7 +244,9 @@ export function IssueEventNavigation({event, group, query}: IssueEventNavigation {TabName[Tab.ATTACHMENTS]} - {hasManyAttachments ? '50+' : attachments.attachments.length} + {hasManyAttachments + ? `${attachments.attachments.length}+` + : attachments.attachments.length} ), diff --git a/static/app/views/issueDetails/streamline/hooks/useIssueDetailsEventCount.tsx b/static/app/views/issueDetails/streamline/hooks/useIssueDetailsEventCount.tsx new file mode 100644 index 00000000000000..027413eaf7f1fd --- /dev/null +++ b/static/app/views/issueDetails/streamline/hooks/useIssueDetailsEventCount.tsx @@ -0,0 +1,28 @@ +import {useMemo} from 'react'; + +import type {Group} from 'sentry/types/group'; +import type {MultiSeriesEventsStats} from 'sentry/types/organization'; +import { + useIssueDetailsDiscoverQuery, + useIssueDetailsEventView, +} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery'; + +export function useIssueDetailsEventCount({group}: {group: Group}) { + const eventView = useIssueDetailsEventView({group}); + const {data: groupStats} = useIssueDetailsDiscoverQuery({ + params: { + route: 'events-stats', + eventView, + referrer: 'issue_details.streamline_graph', + }, + }); + const eventCount = useMemo(() => { + if (!groupStats?.['count()']) { + return 0; + } + return groupStats['count()']?.data?.reduce((count, [_timestamp, countData]) => { + return count + (countData?.[0]?.count ?? 0); + }, 0); + }, [groupStats]); + return eventCount; +} diff --git a/static/app/views/issueDetails/streamline/hooks/useIssueDetailsReplayCount.tsx b/static/app/views/issueDetails/streamline/hooks/useIssueDetailsReplayCount.tsx new file mode 100644 index 00000000000000..8cc3fbfaa2f1a6 --- /dev/null +++ b/static/app/views/issueDetails/streamline/hooks/useIssueDetailsReplayCount.tsx @@ -0,0 +1,58 @@ +import {type Group, IssueCategory} from 'sentry/types/group'; +import { + type ApiQueryKey, + useApiQuery, + type UseApiQueryOptions, +} from 'sentry/utils/queryClient'; +import useOrganization from 'sentry/utils/useOrganization'; +import {useEventQuery} from 'sentry/views/issueDetails/streamline/eventSearch'; +import {useIssueDetailsEventView} from 'sentry/views/issueDetails/streamline/hooks/useIssueDetailsDiscoverQuery'; + +interface ReplayIdsParameters { + orgSlug: string; + query: { + data_source: 'discover' | 'search_issues'; + query: string; + statsPeriod: string; + environment?: string[]; + returnIds?: boolean; + }; +} + +export type ReplayCount = Record; +export type ReplayIds = Record; + +function makeReplayCountQueryKey({orgSlug, query}: ReplayIdsParameters): ApiQueryKey { + return [`/organizations/${orgSlug}/replay-count/`, {query}]; +} + +function useReplayCount( + params: ReplayIdsParameters, + options: Partial> = {} +) { + return useApiQuery(makeReplayCountQueryKey(params), { + staleTime: Infinity, + retry: false, + ...options, + }); +} + +export function useIssueDetailsReplayCount({ + group, +}: { + group: Group; +}) { + const organization = useOrganization(); + const searchQuery = useEventQuery({group}); + const eventView = useIssueDetailsEventView({group}); + return useReplayCount({ + orgSlug: organization.slug, + query: { + data_source: + group.issueCategory === IssueCategory.ERROR ? 'discover' : 'search_issues', + statsPeriod: eventView.statsPeriod ?? '90d', + environment: [...eventView.environment], + query: `issue.id:[${group.id}] ${searchQuery}`, + }, + }); +}