diff --git a/static/app/utils/queryClient.tsx b/static/app/utils/queryClient.tsx index faa41d2c91dfd8..6795b27ef12768 100644 --- a/static/app/utils/queryClient.tsx +++ b/static/app/utils/queryClient.tsx @@ -62,6 +62,7 @@ export type ApiQueryKey = Record, Record >, + additionalKey?: string, ]; export interface UseApiQueryOptions @@ -287,9 +288,11 @@ function parsePageParam(dir: 'previous' | 'next') { export function useInfiniteApiQuery({ queryKey, enabled, + staleTime, }: { queryKey: ApiQueryKey; enabled?: boolean; + staleTime?: number; }) { const api = useApi({persistInFlight: PERSIST_IN_FLIGHT}); const query = useInfiniteQuery({ @@ -299,6 +302,7 @@ export function useInfiniteApiQuery({ getNextPageParam: parsePageParam('next'), initialPageParam: undefined, enabled: enabled ?? true, + staleTime, }); return query; diff --git a/static/app/utils/useReleaseStats.tsx b/static/app/utils/useReleaseStats.tsx new file mode 100644 index 00000000000000..525040d47c5231 --- /dev/null +++ b/static/app/utils/useReleaseStats.tsx @@ -0,0 +1,75 @@ +import {useEffect} from 'react'; + +import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse'; +import {useInfiniteApiQuery} from 'sentry/utils/queryClient'; +import useOrganization from 'sentry/utils/useOrganization'; + +interface ReleaseMetaBasic { + date: string; + version: string; +} + +interface UseReleaseStatsParams { + datetime: Parameters[0]; + environments: readonly string[]; + projects: readonly number[]; + + /** + * Max number of pages to fetch. Default is 10 pages, which should be + * sufficient to fetch "all" releases. + */ + maxPages?: number; +} + +/** + * This is intended to fetch "all" releases, we have a default limit of + * 10 pages (of 1000 results) to be slightly cautious. + */ +export function useReleaseStats( + {datetime, environments, projects, maxPages = 10}: UseReleaseStatsParams, + queryOptions: {staleTime: number} = {staleTime: Infinity} +) { + const organization = useOrganization(); + + const { + isLoading, + isFetching, + fetchNextPage, + hasNextPage, + isPending, + isError, + error, + data, + } = useInfiniteApiQuery({ + queryKey: [ + `/organizations/${organization.slug}/releases/stats/`, + { + query: { + environment: environments, + project: projects, + ...normalizeDateTimeParams(datetime), + }, + }, + // This is here to prevent a cache key conflict between normal queries and + // "infinite" queries. Read more here: https://tkdodo.eu/blog/effective-react-query-keys#caching-data + 'load-all', + ], + ...queryOptions, + }); + + const currentNumberPages = data?.pages.length ?? 0; + + useEffect(() => { + if (!isFetching && hasNextPage && currentNumberPages + 1 < maxPages) { + fetchNextPage(); + } + }, [isFetching, hasNextPage, fetchNextPage, maxPages, currentNumberPages]); + + return { + isLoading, + isPending, + isError, + error, + releases: data?.pages.flatMap(([pageData]) => pageData), + }; +} diff --git a/static/app/views/insights/browser/resources/views/resourcesLandingPage.spec.tsx b/static/app/views/insights/browser/resources/views/resourcesLandingPage.spec.tsx index e88238e58dcff4..4c66388b0e05e5 100644 --- a/static/app/views/insights/browser/resources/views/resourcesLandingPage.spec.tsx +++ b/static/app/views/insights/browser/resources/views/resourcesLandingPage.spec.tsx @@ -27,6 +27,9 @@ jest.mock('sentry/utils/useLocation'); jest.mock('sentry/utils/usePageFilters'); jest.mock('sentry/utils/useProjects'); jest.mock('sentry/views/insights/common/queries/useOnboardingProject'); +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; + +jest.mock('sentry/utils/useReleaseStats'); const requestMocks: Record = {}; @@ -171,6 +174,13 @@ const setupMocks = () => { reloadProjects: jest.fn(), placeholders: [], }); + jest.mocked(useReleaseStats).mockReturnValue({ + isLoading: false, + isPending: false, + isError: false, + error: null, + releases: [], + }); }; const setupMockRequests = (organization: Organization) => { diff --git a/static/app/views/insights/cache/views/cacheLandingPage.spec.tsx b/static/app/views/insights/cache/views/cacheLandingPage.spec.tsx index a636e46b842cae..cdab0a929d2417 100644 --- a/static/app/views/insights/cache/views/cacheLandingPage.spec.tsx +++ b/static/app/views/insights/cache/views/cacheLandingPage.spec.tsx @@ -19,6 +19,9 @@ jest.mock('sentry/utils/useLocation'); jest.mock('sentry/utils/usePageFilters'); jest.mock('sentry/utils/useProjects'); jest.mock('sentry/views/insights/common/queries/useOnboardingProject'); +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; + +jest.mock('sentry/utils/useReleaseStats'); const requestMocks = { missRateChart: jest.fn(), @@ -79,6 +82,14 @@ describe('CacheLandingPage', function () { initiallyLoaded: false, }); + jest.mocked(useReleaseStats).mockReturnValue({ + isLoading: false, + isPending: false, + isError: false, + error: null, + releases: [], + }); + beforeEach(function () { jest.clearAllMocks(); setRequestMocks(organization); diff --git a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx index 05a2f261b47c8f..a175daea8e8725 100644 --- a/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx +++ b/static/app/views/insights/common/components/insightsTimeSeriesWidget.tsx @@ -2,11 +2,11 @@ import styled from '@emotion/styled'; import {openInsightChartModal} from 'sentry/actionCreators/modal'; import {Button} from 'sentry/components/button'; -import ReleaseSeries from 'sentry/components/charts/releaseSeries'; import {CHART_PALETTE} from 'sentry/constants/chartPalette'; import {IconExpand} from 'sentry/icons'; import {t} from 'sentry/locale'; import usePageFilters from 'sentry/utils/usePageFilters'; +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; import {MISSING_DATA_MESSAGE} from 'sentry/views/dashboards/widgets/common/settings'; import type {Aliases} from 'sentry/views/dashboards/widgets/common/types'; import { @@ -38,8 +38,12 @@ export interface InsightsTimeSeriesWidgetProps { export function InsightsTimeSeriesWidget(props: InsightsTimeSeriesWidgetProps) { const pageFilters = usePageFilters(); - const {start, end, period, utc} = pageFilters.selection.datetime; - const {projects, environments} = pageFilters.selection; + const {releases: releasesWithDate} = useReleaseStats(pageFilters.selection); + const releases = + releasesWithDate?.map(({date, version}) => ({ + timestamp: date, + version, + })) ?? []; const visualizationProps: TimeSeriesWidgetVisualizationProps = { visualizationType: props.visualizationType, @@ -108,33 +112,12 @@ export function InsightsTimeSeriesWidget(props: InsightsTimeSeriesWidgetProps) { openInsightChartModal({ title: props.title, children: ( - - {({releases}) => { - return ( - - ({ - timestamp: release.date, - version: release.version, - })) - : [] - } - /> - - ); - }} - + + + ), }); }} diff --git a/static/app/views/insights/database/views/databaseLandingPage.spec.tsx b/static/app/views/insights/database/views/databaseLandingPage.spec.tsx index ab830349c0b2f1..19ad1e41ada697 100644 --- a/static/app/views/insights/database/views/databaseLandingPage.spec.tsx +++ b/static/app/views/insights/database/views/databaseLandingPage.spec.tsx @@ -13,6 +13,9 @@ jest.mock('sentry/utils/useLocation'); jest.mock('sentry/utils/usePageFilters'); jest.mock('sentry/utils/useProjects'); jest.mock('sentry/views/insights/common/queries/useOnboardingProject'); +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; + +jest.mock('sentry/utils/useReleaseStats'); describe('DatabaseLandingPage', function () { const organization = OrganizationFixture({features: ['insights-initial-modules']}); @@ -60,6 +63,14 @@ describe('DatabaseLandingPage', function () { key: '', }); + jest.mocked(useReleaseStats).mockReturnValue({ + isLoading: false, + isPending: false, + isError: false, + error: null, + releases: [], + }); + beforeEach(function () { MockApiClient.addMockResponse({ url: '/organizations/org-slug/sdk-updates/', diff --git a/static/app/views/insights/database/views/databaseSpanSummaryPage.spec.tsx b/static/app/views/insights/database/views/databaseSpanSummaryPage.spec.tsx index 3062081e9c43e6..dc13db23a3613c 100644 --- a/static/app/views/insights/database/views/databaseSpanSummaryPage.spec.tsx +++ b/static/app/views/insights/database/views/databaseSpanSummaryPage.spec.tsx @@ -10,6 +10,9 @@ import {DatabaseSpanSummaryPage} from 'sentry/views/insights/database/views/data jest.mock('sentry/utils/useLocation'); jest.mock('sentry/utils/usePageFilters'); +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; + +jest.mock('sentry/utils/useReleaseStats'); describe('DatabaseSpanSummaryPage', function () { const organization = OrganizationFixture({ @@ -44,6 +47,14 @@ describe('DatabaseSpanSummaryPage', function () { key: '', }); + jest.mocked(useReleaseStats).mockReturnValue({ + isLoading: false, + isPending: false, + isError: false, + error: null, + releases: [], + }); + beforeEach(function () { jest.clearAllMocks(); }); diff --git a/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx b/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx index a0d959aee02f5a..d842b8eba1ddb7 100644 --- a/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx +++ b/static/app/views/insights/http/views/httpDomainSummaryPage.spec.tsx @@ -13,6 +13,9 @@ import {HTTPDomainSummaryPage} from 'sentry/views/insights/http/views/httpDomain jest.mock('sentry/utils/useLocation'); jest.mock('sentry/utils/usePageFilters'); +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; + +jest.mock('sentry/utils/useReleaseStats'); describe('HTTPSummaryPage', function () { const organization = OrganizationFixture({features: ['insights-initial-modules']}); @@ -52,6 +55,14 @@ describe('HTTPSummaryPage', function () { key: '', }); + jest.mocked(useReleaseStats).mockReturnValue({ + isLoading: false, + isPending: false, + isError: false, + error: null, + releases: [], + }); + beforeEach(function () { jest.clearAllMocks(); diff --git a/static/app/views/insights/http/views/httpLandingPage.spec.tsx b/static/app/views/insights/http/views/httpLandingPage.spec.tsx index 028fc788def81d..c8314fccdaa1b0 100644 --- a/static/app/views/insights/http/views/httpLandingPage.spec.tsx +++ b/static/app/views/insights/http/views/httpLandingPage.spec.tsx @@ -13,6 +13,9 @@ jest.mock('sentry/utils/useLocation'); jest.mock('sentry/utils/usePageFilters'); jest.mock('sentry/utils/useProjects'); jest.mock('sentry/views/insights/common/queries/useOnboardingProject'); +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; + +jest.mock('sentry/utils/useReleaseStats'); describe('HTTPLandingPage', function () { const organization = OrganizationFixture({ @@ -75,6 +78,14 @@ describe('HTTPLandingPage', function () { initiallyLoaded: false, }); + jest.mocked(useReleaseStats).mockReturnValue({ + isLoading: false, + isPending: false, + isError: false, + error: null, + releases: [], + }); + beforeEach(function () { jest.clearAllMocks(); diff --git a/static/app/views/insights/queues/charts/latencyChart.spec.tsx b/static/app/views/insights/queues/charts/latencyChart.spec.tsx index 7a44014f937e91..8dcdee4afbd61f 100644 --- a/static/app/views/insights/queues/charts/latencyChart.spec.tsx +++ b/static/app/views/insights/queues/charts/latencyChart.spec.tsx @@ -2,12 +2,23 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import {render, screen, waitForElementToBeRemoved} from 'sentry-test/reactTestingLibrary'; +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; import {LatencyChart} from 'sentry/views/insights/queues/charts/latencyChart'; import {Referrer} from 'sentry/views/insights/queues/referrers'; +jest.mock('sentry/utils/useReleaseStats'); + describe('latencyChart', () => { const organization = OrganizationFixture(); + jest.mocked(useReleaseStats).mockReturnValue({ + isLoading: false, + isPending: false, + isError: false, + error: null, + releases: [], + }); + let eventsStatsMock: jest.Mock; beforeEach(() => { diff --git a/static/app/views/insights/queues/charts/throughputChart.spec.tsx b/static/app/views/insights/queues/charts/throughputChart.spec.tsx index 916cd6f7ba24b3..62da32d53f55e3 100644 --- a/static/app/views/insights/queues/charts/throughputChart.spec.tsx +++ b/static/app/views/insights/queues/charts/throughputChart.spec.tsx @@ -2,14 +2,25 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import {render, screen, waitForElementToBeRemoved} from 'sentry-test/reactTestingLibrary'; +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; import {ThroughputChart} from 'sentry/views/insights/queues/charts/throughputChart'; import {Referrer} from 'sentry/views/insights/queues/referrers'; +jest.mock('sentry/utils/useReleaseStats'); + describe('throughputChart', () => { const organization = OrganizationFixture(); let eventsStatsMock!: jest.Mock; + jest.mocked(useReleaseStats).mockReturnValue({ + isLoading: false, + isPending: false, + isError: false, + error: null, + releases: [], + }); + beforeEach(() => { eventsStatsMock = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events-stats/`, diff --git a/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx b/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx index 7275cd0f2d6e3b..065a8ce4dee23f 100644 --- a/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx +++ b/static/app/views/insights/queues/views/destinationSummaryPage.spec.tsx @@ -5,11 +5,13 @@ import {render, screen, waitForElementToBeRemoved} from 'sentry-test/reactTestin import {useLocation} from 'sentry/utils/useLocation'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; import PageWithProviders from 'sentry/views/insights/queues/views/destinationSummaryPage'; jest.mock('sentry/utils/useLocation'); jest.mock('sentry/utils/usePageFilters'); jest.mock('sentry/utils/useProjects'); +jest.mock('sentry/utils/useReleaseStats'); describe('destinationSummaryPage', () => { const organization = OrganizationFixture({ @@ -54,6 +56,14 @@ describe('destinationSummaryPage', () => { initiallyLoaded: false, }); + jest.mocked(useReleaseStats).mockReturnValue({ + isLoading: false, + isPending: false, + isError: false, + error: null, + releases: [], + }); + let eventsMock: jest.Mock; let eventsStatsMock: jest.Mock; diff --git a/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx b/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx index c9f3a876c0887b..5b9d32bc1fd058 100644 --- a/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx +++ b/static/app/views/insights/queues/views/queuesLandingPage.spec.tsx @@ -6,11 +6,13 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; import {useLocation} from 'sentry/utils/useLocation'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; +import {useReleaseStats} from 'sentry/utils/useReleaseStats'; import QueuesLandingPage from 'sentry/views/insights/queues/views/queuesLandingPage'; jest.mock('sentry/utils/useLocation'); jest.mock('sentry/utils/usePageFilters'); jest.mock('sentry/utils/useProjects'); +jest.mock('sentry/utils/useReleaseStats'); describe('queuesLandingPage', () => { const organization = OrganizationFixture({ @@ -58,6 +60,14 @@ describe('queuesLandingPage', () => { initiallyLoaded: false, }); + jest.mocked(useReleaseStats).mockReturnValue({ + isLoading: false, + isPending: false, + isError: false, + error: null, + releases: [], + }); + let eventsMock: jest.Mock; let eventsStatsMock: jest.Mock;